Class validation on response body in .NET WebApi

73 Views Asked by At

I would like to apply class validation on the response body/output of my .NET WebApi in the same way .NET automatically validates the request body using class validation.

public class RequestAndResponseDto
{
   [Required]
   public string SomeProperty { get; set; }
}

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    [HttpPost]
    Task<RequestAndResponseDto> Post([FromBody] RequestAndResponseDto requestBody)
    {
         return new RequestAndResponseDto();
    }
}

I would like an exception to be thrown, because SomeProperty is a required property. The same would go for other validations, such as MinLength etc.

The solution should be applied to all controllers and all methods in the application. How would I achieve this without adding custom logic in each endpoint?

2

There are 2 best solutions below

9
devandholmes On BEST ANSWER

To apply class validation on the response body/output globally across all controllers and methods in your .NET Web API, you can create and register an action filter that performs model validation on the action result. This approach allows you to reuse the validation logic without modifying each endpoint individually.

Here's how you can achieve this:

  1. Create a Custom Action Filter for Response Validation

First, create a custom action filter that checks if the action result is of the type that needs validation (e.g., your DTOs) and then performs the validation using the Validator class.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.ComponentModel.DataAnnotations;

public class ValidateResponseAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is ObjectResult objectResult)
        {
            var result = objectResult.Value;
            var validationContext = new ValidationContext(result, serviceProvider: null, items: null);
            var validationResults = new List<ValidationResult>();

            bool isValid = Validator.TryValidateObject(result, validationContext, validationResults, true);

            if (!isValid)
            {
                context.Result = new BadRequestObjectResult(validationResults);
            }
        }
    }
}
  1. Register the Custom Action Filter Globally

Next, you need to register this action filter globally so it's applied to all controllers and actions. You can do this in the Startup.cs or wherever you configure services in your application.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.Filters.Add(new ValidateResponseAttribute()); // Register the filter globally
    });
    // Other service configurations...
}

This setup ensures that your response DTOs are validated just like your request DTOs. If the validation fails, a BadRequest response is returned with the validation errors. Note: This approach validates the response before it's sent back to the client. It's important to ensure that your application logic correctly handles cases where the response data might not pass validation, as this could indicate a flaw in your application's data handling or business logic.

0
Jeandre Van Dyk On

You can just add this in your controller endpoint if you want to validate it on an endpoint level.

[HttpPost]
public async Task<ActionResult<RequestAndResponseDto>> Post([FromBody] RequestAndResponseDto requestBody)
{
    if (!ModelState.IsValid)
    {
        return UnprocessableEntity(ModelState);
    }
    return new RequestAndResponseDto();
}

Error response

public class RequestAndResponseDto
{
    [MinLength(1)]
    [Required(ErrorMessage = "some error")]
    public string SomeProperty { get; set; }
}
or you could also add it to a middleware:

public class ModelValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ModelValidationMiddleware> _logger;

    public ModelValidationMiddleware(RequestDelegate next, ILogger<ModelValidationMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments("/api")) // Assuming API endpoints for validation
        {
            // Read request body
            string requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync();

            // Deserialize request body into model
            var model = JsonConvert.DeserializeObject<RequestAndResponseDto>(requestBody);

            // Validate model using data annotations
            var validationResults = new List<ValidationResult>();
            var isValid = Validator.TryValidateObject(model, new ValidationContext(model), validationResults, true);

            if (!isValid)
            {
                _logger.LogWarning("Model validation failed.");

                // Collect validation errors
                var errors = new List<string>();
                foreach (var validationResult in validationResults)
                {
                    foreach (var memberName in validationResult.MemberNames)
                    {
                        errors.Add($"{memberName}: {validationResult.ErrorMessage}");
                    }
                }

                // Return validation errors in the response
                var errorResponse = new { Errors = errors };
                var jsonResponse = JsonConvert.SerializeObject(errorResponse);

                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(jsonResponse);
                return;
            }
        }

        await _next(context);
    }
}
Remember to add the middleware to the App:

builder.UseMiddleware<ModelValidationMiddleware>()