How to make "One or more validation errors occurred" raise an exception?

Viewed 29899

I'm running a WebAPI on Core 3.1 and one of my endpoints excepts JSON with a model that has fields with [Required] attribute like so:

public class Vendor
{
        public int ID { get; set; }
        [Required(ErrorMessage = "UID is required")]   <<<------ required attribute
        public string UID { get; set; }
}

When i call this endpoint without setting UID I get the following output as expected:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|7ced8b82-4aa34d65daa99a12.",
    "errors": {
        "Vendor.UID": [
            "UID is required"
        ]
    }
}

Altho this output is pretty informative and clear it is not consistent with other error outputs that my API produces by means of ExceptionFilter. Is there any way this errors can be routed to the exception filter as well?

Here is my Startup.cs:

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            Common.Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
            services.AddMvc().AddXmlDataContractSerializerFormatters();
            services.AddMvc().AddMvcOptions(options =>
            {
                options.EnableEndpointRouting = false;
            });
            services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
            services.AddOptions();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
        {

            //Middleware for exception filtering
            app.UseMiddleware<ErrorHandlingMiddleware>(new ErrorHandlingMiddlewareOptions { logger = logger });

            app.UseStaticFiles();            

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute("EndpointNotFound", "{*url}", new { controller = "Error", action = "EndpointNotFound" });
            });
        }
    }
5 Answers

You can add filter in your mvc service or controller service

this filter return badrequest

services.AddControllers(option =>
{
             option.Filters.Add<ValidationFilter>();
});

to create your filter you can add this class also you can customize this filter to what ever you want

public class ValidationFilter : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            //before contrller

            if(!context.ModelState.IsValid)
            {
                var errorsInModelState = context.ModelState
                    .Where(x => x.Value.Errors.Count > 0)
                    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray());

                var errorResponse = new ErrorResponse();

                foreach (var error in errorsInModelState)
                {
                    foreach (var subError in error.Value)
                    {
                        var errorModel = new ErrorModel
                        {
                            FieldName = error.Key,
                            Message = subError
                        };

                        errorResponse.Error.Add(errorModel);
                    }

                    context.Result = new BadRequestObjectResult(errorResponse);
                    return;
                }

                await next();

                //after controller  
            }
        }
    }

I have created error model just like this

public class ErrorModel
    {
        public string FieldName { get; set; }
        public string Message { get; set; }
    }

and error response like below

public class ErrorResponse
    {
        public List<ErrorModel> Error { get; set; } = new List<ErrorModel>();
        public bool Successful { get; set; }
    }

I have just change in http methos and work fine for the same issue

[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)

You want to output the JSON like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|7ced8b82-4aa34d65daa99a12.",
    "errors": {
        "Vendor.UID": [
            "UID is required"
        ]
    }
}
  1. the type and the status field, are from here https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs#L54-L108, the ClientErrorMapping will be configured when the dotnet core project setup.

  2. the JSON was a ValidationProblemDetails which created by the DefaultProblemDetailsFactory, https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs#L45

  3. we can use this ProblemDetailsFactory to create the ValidationProblemDetails https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L261

You might want to take a look at the great library FluentValidation!

Sample:

Build a validator module binding your DTO and create a set of rules.

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(x => x.Surname).NotEmpty();
    RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
    RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
    RuleFor(x => x.Address).Length(20, 250);
    RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
  }

  private bool BeAValidPostcode(string postcode) {
    // custom postcode validating logic goes here
  }
}

Inject it in your controllers through DI:

    services.AddControllers()
                .AddFluentValidation(s =>
                {
                    s.ValidatorOptions.CascadeMode = CascadeMode.Stop;
                    s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
                    s.ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US");
                    s.RegisterValidatorsFromAssemblyContaining<Customer>();
                    
                    ...
                    // more validators
                });
  • That way your code looks well organized;
  • You get rid of Data Annotations spread all over your code.
  • Personalize error messages and validations.

You also may want to check why implementing ControllerBase on your controllers might be the way to go while using web APIs.

Related