2
votes

I am trying to implement service gateway pattern according to Service Gateway tutorial to support in-process handing via InProcessServiceGateway and external calling via JsonServiceClient in case ServiceStack service is deployed standalone. I use ServiceStack 4.5.8 version.

Validation feature works fine, but with InProcessServiceGateway, the failed validation throws ValidationException which directly results in a ServiceStack.FluentValidation.ValidationException in the client rather than populating ResponseStatus property of MyResponseDto. And I also tried GlobalRequestFilters and ServiceExceptionHandlers, both of them seem to work fine to capture ValidationException only with JsonHttpClient but InProcessServiceGateway.

Is there any way to make ValidationException thrown by InProcessServiceGateway captured and translated into Dto's ResponseStatus? Thanks.

My AppHost:

   //Register CustomServiceGatewayFactory
   container.Register<IServiceGatewayFactory>(x => new CustomServiceGatewayFactory()).ReusedWithin(ReuseScope.None);

   //Validation feature
   Plugins.Add(new ValidationFeature());

   //Note: InProcessServiceGateway cannot reach here.
   GlobalRequestFilters.Add((httpReq, httpRes, requestDto) =>
   {
     ...
   });

   //Note: InProcessServiceGateway cannot reach here.
   ServiceExceptionHandlers.Add((httpReq, request, ex) =>
   {
     ...
   });

My CustomServiceGatewayFactory:

public class CustomServiceGatewayFactory : ServiceGatewayFactoryBase
{
    private IRequest _req;

    public override IServiceGateway GetServiceGateway(IRequest request)
    {
        _req = request;

        return base.GetServiceGateway(request);
    }

    public override IServiceGateway GetGateway(Type requestType)
    {
        var standaloneHosted = false;
        var apiBaseUrl = string.Empty;

        var apiSettings = _req.TryResolve<ApiSettings>();
        if (apiSettings != null)
        {
            apiBaseUrl = apiSettings.ApiBaseUrl;
            standaloneHosted = apiSettings.StandaloneHosted;
        }

        var gateway = !standaloneHosted
            ? (IServiceGateway)base.localGateway
            : new JsonServiceClient(apiBaseUrl)
            {
                BearerToken = _req.GetBearerToken()
            };
        return gateway;
    }
}

My client base controller (ASP.NET Web API):

    public virtual IServiceGateway ApiGateway
    {
        get
        {
            var serviceGatewayFactory = HostContext.AppHost.TryResolve<IServiceGatewayFactory>();
            var serviceGateway = serviceGatewayFactory.GetServiceGateway(HttpContext.Request.ToRequest());
            return serviceGateway;
        }
    }

My client controller action (ASP.NET Web API):

    var response = ApiGateway.Send<UpdateCustomerResponse>(new UpdateCustomer
    {
        CustomerGuid = customerGuid,
        MobilePhoneNumber = mobilePhoneNumber 
        ValidationCode = validationCode
    });

    if (!response.Success)
    {
        return this.Error(response, response.ResponseStatus.Message);
    }

My UpdateCustomer request DTO:

[Route("/customers/{CustomerGuid}", "PUT")]
public class UpdateCustomer : IPut, IReturn<UpdateCustomerResponse>
{
    public Guid CustomerGuid { get; set; }

    public string MobilePhoneNumber { get; set; }

    public string ValidationCode { get; set; }
}

My UpdateCustomerValidator:

public class UpdateCustomerValidator : AbstractValidator<UpdateCustomer>
{
    public UpdateCustomerValidator(ILocalizationService localizationService)
    {
        ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure;

        RuleFor(x => x.ValidationCode)
            .NotEmpty()
            .When(x => !string.IsNullOrWhiteSpace(x.MobilePhoneNumber))
            .WithErrorCode(((int)ErrorCode.CUSTOMER_VALIDATIONCODE_EMPTY).ToString())
            .WithMessage(ErrorCode.CUSTOMER_VALIDATIONCODE_EMPTY.GetLocalizedEnum(localizationService, Constants.LANGUAGE_ID));
    }
}

My UpdateCustomerResponse DTO:

public class UpdateCustomerResponse
{
    /// <summary>
    /// Return true if successful; return false, if any error occurs.
    /// </summary>
    public bool Success { get; set; }

    /// <summary>
    /// Represents error details, populated only when any error occurs.
    /// </summary>
    public ResponseStatus ResponseStatus { get; set; }
}

ServiceStack 4.5.8's InProcessServiceGateway source code:

private TResponse ExecSync<TResponse>(object request)
{
    foreach (var filter in HostContext.AppHost.GatewayRequestFilters)
    {
        filter(req, request);
        if (req.Response.IsClosed)
            return default(TResponse);
    }

    if (HostContext.HasPlugin<ValidationFeature>())
    {
        var validator = ValidatorCache.GetValidator(req, request.GetType());
        if (validator != null)
        {
            var ruleSet = (string)(req.GetItem(Keywords.InvokeVerb) ?? req.Verb);
            var result = validator.Validate(new ValidationContext(
                request, null, new MultiRuleSetValidatorSelector(ruleSet)) {
                Request = req
            });
            if (!result.IsValid)
                throw new ValidationException(result.Errors);
        }
    }

    var response = HostContext.ServiceController.Execute(request, req);
    var responseTask = response as Task;
    if (responseTask != null)
        response = responseTask.GetResult();

    return ConvertToResponse<TResponse>(response);
}
1

1 Answers

1
votes

ServiceStack's Service Gateways now convert validation exceptions into WebServiceExceptions from this commit which is available from v4.5.13 that's now available on MyGet.