Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom response DTO in ServiceStack FluentValidation

I am evaluating FluentValidation in ServiceStack for handling automatic validation of request DTOs:

Plugins.Add(new ValidationFeature());
container.RegisterValidators(typeof(MyValidator).Assembly);

Errors are returned to the client by serializing an ErrorResponse DTO and may look like this:

{
    "ErrorCode": "GreaterThan",
    "Message": "'Age' must be greater than '0'.",
    "Errors": [
        {
            "ErrorCode": "GreaterThan",
            "FieldName": "Age",
            "Message": "'Age' must be greater than '0'."
        },
        {
            "ErrorCode": "NotEmpty",
            "FieldName": "Company",
            "Message": "'Company' should not be empty."
        }
    ]
}

I would like to know if it is possible to return the errors using a different response DTO. For example:

{
    "code": "123",
    "error": "'Age' must be greater than '0'."
}

I know that it is possible to use the validator explicitly in the service:

public MyService : Service
{
    private readonly IValidator<MyRequestDto> validator;
    public MyService(IValidator<MyRequestDto> validator)
    {
        this.validator = validator;
    }

    public object Get(MyRequestDto request)
    {
        var result = this.validator.Validate(request);
        if (!result.IsValid)
        {
            throw new SomeCustomException(result);
        }

        ... at this stage request DTO validation has passed
    }
}

But the question here is whether it is possible to have this validation error intercepted implicitly somewhere so that I can replace the response DTO and have a cleaner service:

public MyService : Service
{
    public object Get(MyRequestDto request)
    {
        ... at this stage request DTO validation has passed
    }
}

UPDATE:

After further digging into the source code, it looks like that this is burnt into the ValidationFeature and more specifically the request filter that it registers:

public class ValidationFilters
{
    public void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
    {
        var validator = ValidatorCache.GetValidator(req, requestDto.GetType());
        if (validator == null) return;

        var validatorWithHttpRequest = validator as IRequiresHttpRequest;
        if (validatorWithHttpRequest != null)
            validatorWithHttpRequest.HttpRequest = req;

        var ruleSet = req.HttpMethod;
        var validationResult = validator.Validate(
        new ValidationContext(requestDto, null, new MultiRuleSetValidatorSelector(ruleSet)));

        if (validationResult.IsValid) return;

        var errorResponse = DtoUtils.CreateErrorResponse(
            requestDto, validationResult.ToErrorResult());

        res.WriteToResponse(req, errorResponse);
    }
}

By writing a custom validation feature I was able to achieve the desired effect. But maybe there's a more elegant way?

like image 760
Darin Dimitrov Avatar asked Apr 17 '13 07:04

Darin Dimitrov


1 Answers

I've just checked in a Customization Error hook that allows you to specify a custom error filter to make it easier to support this use-case in the next version of ServiceStack (v3.9.44+).

From the CustomValidationErrorTests you can now configure the ValidationFeature to use a custom error filter which will be returned instead, e.g:

public override void Configure(Container container)
{
    Plugins.Add(new ValidationFeature { 
        ErrorResponseFilter = CustomValidationError });
    container.RegisterValidators(typeof(MyValidator).Assembly);           
}

public static object CustomValidationError(
    ValidationResult validationResult, object errorDto)
{
    var firstError = validationResult.Errors[0];
    var dto = new MyCustomErrorDto { 
        code = firstError.ErrorCode, error = firstError.ErrorMessage };

    //Ensure HTTP Clients recognize this as an HTTP Error
    return new HttpError(dto, HttpStatusCode.BadRequest, dto.code, dto.error);
}

Now your HTTP Clients will receive your customized error response:

try
{
    var response = "http://example.org/customerror".GetJsonFromUrl();
}
catch (Exception ex)
{
    ex.GetResponseBody().Print(); 
    //{"code":"GreaterThan","error":"'Age' must be greater than '0'."}
}

Warning: When customizing the Error Response in this way ServiceStack's typed C# clients will no longer provide typed exceptions as they expect the Error Response to contain a ResponseStatus DTO property.

like image 68
mythz Avatar answered Oct 03 '22 10:10

mythz