How to write a ServiceStack plugin which needs bot

2019-04-09 09:33发布

问题:

I need to service localized data. All response Dtos which are localized share the same properties. I.e. I defined an interface (ILocalizedDto) to mark those Dtos. On the request side, there is a ILocalizedRequest for requests which demand localization.

Using IPlugin I already managed to implement the required feature. However I am quite sure that the implementation is not thread safe and additionally I don't know if I could use IHttpRequest.GetHashCode() as identifier for one request/response cycle.

What would be the correct way to implement a ServiceStack plugin which makes use of both request and response Dto? I.e. is there some IHttpRequest.Context to store data in or is it possible to get the request dto at response time?

internal class LocalizationFeature : IPlugin
{
    public static bool Enabled { private set; get; }

    /// <summary>
    ///     Activate the localization mechanism, so every response Dto which is a <see cref="ILocalizedDto" />
    ///     will be translated.
    /// </summary>
    /// <param name="appHost">The app host</param>
    public void Register(IAppHost appHost)
    {
        if (Enabled)
        {
            return;
        }
        Enabled = true;
        var filter = new LocalizationFilter();
        appHost.RequestFilters.Add(filter.RequestFilter);
        appHost.ResponseFilters.Add(filter.ResponseFilter);
    }
}

// My request/response filter
public class LocalizationFilter
{
    private readonly Dictionary<int,ILocalizedRequest> localizedRequests = new Dictionary<int, ILocalizedRequest>();

    public ILocalizer Localizer { get; set; }

    public void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
    {
        var localizedRequest = requestDto as ILocalizedRequest;
        if (localizedRequest != null)
        {
            localizedRequests.Add(GetRequestId(req), localizedRequest);
        }
    }

    public void ResponseFilter(IHttpRequest req, IHttpResponse res, object response)
    {
        var requestId = GetRequestId(req);
        if (!(response is ILocalizedDto) || !localizedRequests.ContainsKey(requestId))
        {
            return;
        }

        var localizedDto = response as ILocalizedDto;
        var localizedRequest = localizedRequests[requestId];
        localizedRequests.Remove(requestId);

        Localizer.Translate(localizedDto, localizedRequest.Language);
    }

    private static int GetRequestId(IHttpRequest req)
    {
        return req.GetHashCode();
    }
}

回答1:

First off: Why do you need the request itself, when all you need is the value of the Language property?

The following code is a legit solution for your scenario:

public class LocalizationFeature : IPlugin
{
  public const string LanguageKey = "X-Language";

  public void Register(IAppHost appHost)
  {
    this.GlobalRequestFilters.Add(this.InterceptRequest);
    this.GlobalResponseFilters.Add(this.InterceptResponse);
  }

  private void InterceptRequest(IRequest request,
                                IResponse response,
                                object dto)
  {
    var localizedRequest = dto as ILocalizedRequest;
    if (localizedRequest != null)
    {
      request.SetItem(LanguageKey,
                      localizedRequest.Language);
    }
  }

  private void InterceptResponse(IRequest request,
                                 IResponse response,
                                 object dto)
  {
    var localizedDto = dto as ILocalizedDto;
    if (localizedDto != null)
    {
      var language = request.GetParam(LanguageKey) ?? request.GetItem(LanguageKey);
      if (!string.IsNullOrEmpty(language))
      {
        Localizer.Translate(localizedDto,
                            language);
      }
    }
  }
}

The interesting thing here is var language = request.GetParam(LanguageKey) ?? request.GetItem(LanguageKey);, which gives you the opportunity to inject the language with either an HTTP header, cookie, or form-data (if applicable) with the key "X-Language". (You could also do another ?? DefaultLanguage to inject a default language for translating ...)

If it is not supplied this way, the set Language of the request is read from the Items of the request and used.

Edit: Additionally, as pointed out by @mythz, you can also access the request DTO with request.Dto in the InterceptResponse method:

private void InterceptResponse(IRequest request,
                               IResponse response,
                               object dto)
{
    var localizedRequest = request.Dto as ILocalizedRequest;
    var localizedDto = dto as ILocalizedDto;
    if (localizedRequest != null
        && localizedDto != null)
    {
      Localizer.Translate(localizedDto,
                          localizedRequest.Language);
    }
}