1

Is there a mechanism for returning a custom error response if an invalid type is given to a WebApi in Dotnet Core?

E.G.

if I have a class that looks like this

public class SomeApiClass
{
    public int Id { get; set; }
}

But make a post request like this (notice that I'm expecting an int and giving a string):

{
    "id": "f"
}

Then the standard dotnet response looks like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-27be45d9cffab14698524a63120a4f88-6bfe2613f2328a42-00",
    "errors": {
        "$.id": [
            "The JSON value could not be converted to System.Int64. Path: $.wmdaid | LineNumber: 1 | BytePositionInLine: 15."
        ]
    }
}

However, I'd like all my responses to look the same for bad requests so that anybody implementing the API can always get a consistent JSON back. My problem being the JSON deserialisation is being done before the controller validation.

So, in a nutshell, Is it possible to change this response format as a part of the dotnet middleware?

scottdavidwalker
  • 1,118
  • 1
  • 12
  • 30
  • You need to replace the default model binder/input formatter for JSON requests to intercept & replace the error message before it reaches the controller action. https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#customize-model-binding-with-input-formatters – abdusco Jul 22 '21 at 13:23

4 Answers4

4

You can use custom ActionFilter.

public class ReformatValidationProblemAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is BadRequestObjectResult badRequestObjectResult)
            if (badRequestObjectResult.Value is ValidationProblemDetails)
            {
                context.Result = new BadRequestObjectResult("Custom Result Here");
            }

        base.OnResultExecuting(context);
    }
}

Controller.cs

[ApiController]
[ReformatValidationProblem]
public class Controller : ControllerBase
{ 
    ...
}

or register it globally Startup.cs

services.AddMvc(options =>
{
    options.Filters.Add(typeof(ReformatValidationProblemAttribute));
});
MrMoeinM
  • 2,080
  • 1
  • 11
  • 16
2

You can also configure JsonOptions so that a generic message is displayed instead.

builder.Services.AddControllers().AddJsonOptions(o => o.AllowInputFormatterExceptionMessages = false);

This will preserve all the other fields from the automatic 400 response (from ValidationProblem) and display a generic error message for the malformed field: "The input was not valid."

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-190787f7ecae2a19b0e9c5da9c270fad-42237ddd1f920665-00",
    "errors": {
        "$.id": [
            "The input was not valid."
        ]
    }
}
Orestis P.
  • 805
  • 7
  • 27
0

You can implement your custom error model handler and replace it with the default ModelState object in each scenario such as bad request or ... First of all change service provider to inject your custom error class like following :

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)    
  .ConfigureApiBehaviorOptions(options => {      
   options.InvalidModelStateResponseFactory = actionCtx => {  
    return CustomBadRequestHandler(actionCtx);  
   };  
  });  

Next, we are going to create our custom Error Handler Class:

private BadRequestObjectResult CustomBadRequestHandler(ActionContext actionCtx) {      
 return new BadRequestObjectResult(actionCtx.ModelState  
  .Where(modelError => modelError.Value.Errors.Count > 0)  
  .Select(modelError => new Error {  
   ErrorFieldKey = modelError.Key,  
    ErrorDescription = modelError.Value.Errors.FirstOrDefault().ErrorMessage  
  }).ToList());  
} 

And finally, our custom error model that we want to return:

public class Error    
{    
    //your custom filed that you want to show
    public string ErrorFieldKey { get; set; }    
    public string ErrorDescription { get; set; }         
}  
masoud
  • 355
  • 2
  • 10
0

.NET 6.0

You can do the following:

  1. Create a custom validation attribute: PositiveInt32ValidationAttribute.cs.
  2. Create a custom result filter: MyApiResultFilter.cs.
  3. Register this result filter in Program.cs.

Here is a sample example:

PositiveInt32ValidationAttribute.cs

public class PositiveInt32ValidationAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(Object? value, ValidationContext validationContext)
    {
        var memberNames = new List<String> { validationContext.MemberName! };
        var errorMessage = String.Format("Invalid {0}.", validationContext.MemberName);
        if (value is Int32 id && id > 1)
            return ValidationResult.Success;
        return new ValidationResult(errorMessage, memberNames);
    }
}

MyApiResultFilter.cs

public class MyApiResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {

    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var count = context.ModelState.Count;
            var errors = new Dictionary<String, String[]>(count);
            foreach (var keyModelStatePair in context.ModelState)
            {
                var key = keyModelStatePair.Key;
                var modelErrors = keyModelStatePair.Value.Errors;
                if (modelErrors is not null && modelErrors.Count > 0)
                {
                    var errorMessages = modelErrors.Select(error => error.ErrorMessage).ToArray();
                    errors.Add(key, errorMessages);
                }
            }

            var response = new
            {
                StatusCode = StatusCodes.Status400BadRequest,    //namespace Microsoft.AspNetCore.Http
                IsSuccess = false,
                Result = errors
            };
            context.Result = new BadRequestObjectResult(response);
        }
    }
}

Registering the MyApiResultFilter in Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddControllers(config => config.Filters.Add(new MyApiResultFilter())
    .AddNewtonsoftJson(options =>    //I am using it to get the response in snake_case JSON format. You don't have to do this.
    {
        options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
        options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };
    });

Result

{
    "status_code": 400,
    "is_success": false,
    "result": {
        "id": [
            "Invalid id."
        ]
    }
}

Screenshot

Result

phougatv
  • 881
  • 2
  • 12
  • 29