简体   繁体   中英

ServiceStack intercepting request/response for validation

I am trying to intercept the Request and Response in ServiceRunner to run validations against them.

1. I am not sure how to abort the request if (!result.IsValid), or if this is a correct approach at all.

2. Especially with response, object response is unknown untill dynamic runtime, it makes me difficult to create IValidator for it which requires known type at compile time.

Please see the comments in code:

public class CustomServiceRunner<T> : ServiceRunner<T> {

    public CustomServiceRunner(IAppHost appHost, ActionContext actionContext) 
        : base(appHost, actionContext) { }

    public override void BeforeEachRequest(IRequestContext requestContext, T request) {
        var validator = AppHost.TryResolve<IValidator<T>>();
        var result = validator.Validate(request);
        //----------------------
        //if (!result.IsValid) 
        //  How to return result.ToResponseDto() and abort the request?
        //----------------------
        base.BeforeEachRequest(requestContext, request);
    }
    public override object AfterEachRequest(IRequestContext requestContext, T request, object response) {
        //-----------------------
        //Validating against response presents a more challenging issue
        //I may have multiple response classes and returned response is 
        //a type object, response class is to be decided dynamically at
        //runtime. I am not sure how to fit this to IValidator<T>
        //-----------------------
        return base.AfterEachRequest(requestContext, request, response);
    }
}

Having validation in a interecpting method is going to make my Service classes code more clean. I thought about Request/Response Filters too, but then I need to write filter tags everywhere. ServiceRunner is nicer because it makes the whole validation process (or AOP in general) transparent to the rest of Services.

I'm not following why using a RequestFilter won't suffice. As described in https://github.com/ServiceStack/ServiceStack/wiki/Validation , do this in AppHost.Configure() :

// Enable the validation feature
Plugins.Add(new ValidationFeature());

// This method scans the assembly for validators
container.RegisterValidators(Assemblies);

This will set up a RequestFilter for Validation and will register all validators defined in the specified assemblies. The RequestFilter will cause exceptions to be thrown instead of invoking your services, if any validation errors are found.

All the wiring you are doing looks redundant (and brittle) to me compared with what ServiceStack can do for you.

Now, that said, I don't if I follow the post-request validation. I guess you're trying to guarantee that the service can't return invalid results, as opposed to guaranteeing that the request can't execute if it is invalid. This seems like something of a corner case to me, where you're not trusting your data repository and you want to completely fail and not even be able to see the bad data (which may make it hard for the consumer to fix).

I also haven't worked yet with response filters so I don't know for sure what limitations they have. But looking at https://github.com/ServiceStack/ServiceStack/wiki/Request-and-response-filters , it seems to show that you can do about the same thing...so it sounds like you can write a different response do the response stream and then close it. I gather this means that the response filter executes after the service, but before the serialization to the response stream occurs. So you should be able to write out a different response and the service's response will be ignored. The code snippet I posted at How to find Service from ServiceStack RequestFilter may help.

If you have an error to return from validation, then just as you're doing, you can throw an exception, or write an error response directly. Look at the code provided by ServiceStack though: download the project and borrow/modify the RequestFilter code in ValidationFilter.cs. Here's the current implementation in case it helps:

public void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
  IValidator validator = ValidatorCache.GetValidator(req, requestDto.GetType());
  if (validator == null)
    return;
  IRequiresHttpRequest requiresHttpRequest = validator as IRequiresHttpRequest;
  if (requiresHttpRequest != null)
    requiresHttpRequest.HttpRequest = req;
  string httpMethod = req.HttpMethod;
  ValidationResult result = validator.Validate(new ValidationContext(requestDto, (PropertyChain) null, (IValidatorSelector) new MultiRuleSetValidatorSelector(new string[1]
  {
    httpMethod
  })));
  if (result.IsValid)
    return;
  object errorResponse = DtoUtils.CreateErrorResponse(requestDto, ValidationResultExtensions.ToErrorResult(result));
  ServiceStack.WebHost.Endpoints.Extensions.HttpResponseExtensions.WriteToResponse(res, req, errorResponse);
}

The response filter should look similar to the request filter, but you'll have to install your own. To do this you will need to implement your own IPlugin. One easy way to do this is just to copy/paste/edit the existing ValidationFeature.cs from https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.ServiceInterface/Validation/ValidationFeature.cs . (The class has some private elements, which make it suboptimal for subclassing, otherwise I would have suggested that.)

The key change you will need to make there will be to register your own filters:

    /// <summary>
    /// Activate the validation mechanism, so every request DTO with an existing validator
    /// will be validated.
    /// </summary>
    /// <param name="appHost">The app host</param>
    public void Register(IAppHost appHost)
    {
        if (Enabled) return;
        Enabled = true;
        // use my class instead of ServiceStack.ServiceInterface.Validation.ValidationFilters
        var filter = new MyValidationFilters();
        appHost.RequestFilters.Add(filter.RequestFilter);
        appHost.ResponseFilters.Add(filter.RequestFilter);
    }

Then you can create your own MyValidationFilters class. Here you can derive from ServiceStack.ServiceInterface.Validation.ValidationFilters if it makes sense, and just use their RequestFilter if that works for you. But your ResponseFilter may need to be subtly different from the RequestFilter because it gets passed the Response DTO instead of the Request DTO. Note this snippet from the RequestFilter:

  object errorResponse = DtoUtils.CreateErrorResponse(requestDto, ValidationResultExtensions.ToErrorResult(result));

This code won't work right because ServiceStack will try to take the requestDto, construct an appropriate Response DTO, and populate that, as you can see in the DtoUtils code:

public static object CreateErrorResponse(object request, ValidationErrorResult validationError)
{
  ResponseStatus responseStatus = DtoUtils.ToResponseStatus(validationError);
  return DtoUtils.CreateErrorResponse(request, (Exception) new ValidationError(validationError), responseStatus);
}

public static object CreateErrorResponse(object request, Exception ex, ResponseStatus responseStatus)
{
  object responseDto = DtoUtils.CreateResponseDto(request, responseStatus);
  IHttpError httpError = ex as IHttpError;
  if (httpError != null)
  {
    if (responseDto != null)
      httpError.Response = responseDto;
    return (object) httpError;
  }
  else
  {
    string errorCode = ex.GetType().Name;
    string errorMessage = ex.Message;
    if (responseStatus != null)
    {
      errorCode = responseStatus.ErrorCode ?? errorCode;
      errorMessage = responseStatus.Message ?? errorMessage;
    }
    return (object) new HttpError(responseDto, HttpRequestExtensions.ToStatusCode(ex), errorCode, errorMessage);
  }
}

Instead you'll need to bypass the CreateResponseDto part (since you already have the Response DTO in the ResponseFilter) and just do the rest.

Note that all of the above copy/paste could be avoided with changes to ServiceStack. You could refactor the ServiceStack code yourself to avoid the duplication and then submit a pull request to github.

(Just a fix for the 100+ if statements, didn't want to place in comment due to code tags)

In your AfterEachRequest, make use of dynamic invocation:

dynamic dynamicResponse = response;
IValidator validator = TryResolveValidator(response); // magic happens here

// ... your error handling code ...

public IValidator<T> TryResolveValidator<T>(T response)
{
    return AppHost.TryResolve<IValidator<T>>();
}

I get this working in an Ugly way . The else if statements in my AfterEachRequest() are Not nice :

public class CustomServiceRunner<T> : ServiceRunner<T> {
    public CustomServiceRunner(IAppHost appHost, ActionContext actionContext) : base(appHost, actionContext) { }
    //1.
    public override void BeforeEachRequest(IRequestContext requestContext, T request) {
        var validator = AppHost.TryResolve<IValidator<T>>();
        if (validator != null) {
            var result = validator.Validate(request);
            if (!result.IsValid) throw result.ToException();
        }
        base.BeforeEachRequest(requestContext, request);
    }
    //2.
    public override object AfterEachRequest(IRequestContext requestContext, T request, object response) {
        IValidator validator = null;
        if(response.GetType()==typeof(CustomersResponse)) validator= AppHost.TryResolve<IValidator<CustomersResponse>>();
        else if(response.GetType()==typeof(OrdersResponse)) validator= AppHost.TryResolve<IValidator<OrdersResponse>>();
        else if(response.GetType()==typeof(LoginResponse)) validator= AppHost.TryResolve<IValidator<LoginResponse>>();
        //......
        // and 100+ more 'ELSE IF' statements o_O ??
        //......
        if (validator != null) {
            var result = validator.Validate(response);
            if (!result.IsValid) throw result.ToException();
        }
        return base.AfterEachRequest(requestContext, request, response);
    }

}

:P ~ Still hoping Mythz or someone experienced can help me improve the solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM