I am writing an API using asp.net core 3.0 and have configured my application with the following behavior for all controllers:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.ConfigureApiBehaviorOptions(setupAction =>
{
setupAction.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Type = "https://courselibrary.com/modelvalidationproblem",
Title = "One or more model validation errors occurred.",
Status = StatusCodes.Status422UnprocessableEntity,
Detail = "See the errors property for details",
Instance = context.HttpContext.Request.Path
};
problemDetails.Extensions.Add("traceId", context.HttpContext.TraceIdentifier);
return new UnprocessableEntityObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" }
};
};
});
...
}
This works great with data annotations on my input (ex: [Required]) class's properties. It returns a 422 Unprocessable Entity reponse like this if any annotations fail:
{
"type": "https://courselibrary.com/modelvalidationproblem",
"title": "One or more model validation errors occurred.",
"status": 422,
"detail": "See the errors property for details",
"instance": "/api/songbooks/21924d66-dac6-43a5-beee-206de4d35216/songs",
"traceId": "0HLQFGSFIFL5L:00000001",
"errors": {
"Title": [
"The Title field is required."
]
}
}
However, I am implementing FluentValudation in my controller like this:
if (!newSong.IsValid)
{
newSong.Validate().AddToModelState(ModelState, null);
_logger.LogWarning("{method} failed model validation (ModelState: {#modelState}), returning Unprocessable Entity", nameof(Post), ModelState.Values.SelectMany(v => v.Errors));
return UnprocessableEntity(ModelState);
}
However, this doesn't trigger the InvalidModelStateResponseFactory like the built-in validation does.
Does anyone know how I can trigger the same sort of event from within my controller to use this convenient handler?
In fact, there is a pretty simple answer. Rather than returning an ActionResult of Ok() or BadRequest() or whatnot, by using this ValidationProblem pattern, you can simply return ValidationProblem(ModelState) and it'll use the factory set up in the Startup.cs Api configuration!
So here's what I can do instead:
if (!newSong.IsValid)
{
newSong.Validate().AddToModelState(ModelState, null);
_logger.LogWarning("{method} failed model validation (ModelState: {#modelState}), returning Unprocessable Entity", nameof(Post), ModelState.Values.SelectMany(v => v.Errors));
return ValidationProblem(ModelState);
}
In addition, you'll need to override the behavior to pull it out from your startup like this (Thanks to Kevin Dockx for this idea):
public override ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
{
var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>();
return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext);
}
Related
How to catch this error in the back-end C# code?
See the solution usedat the end of the question description.
It is due to integer overflow for a HTTP GET URL where the URL contains an integer parameter. Calls with a 32 bit integer sized value work (e.g., "1234" works and the API is called).
HTTP GET www.domain.net/api/product/{productIdInteger}
Where the JavaScript caller calls HTTPS
www.domain.net/api/product/222222222222222222222222222222222222
The response looks like:
{
"errors": {
"productIdInteger": [
"The value '222222222222222222222222222222222222' is not valid."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-f6a8a724853f3f44ba9fc6468a42ad94-aca067537e73b645-00"
}
Startup.cs adds a middleware to catch exceptions at the WebAPI controller level.
This does not catch the URL parameter parsing error or a bad JSON in HTTP Body parsing error
Startup.cs has app.UseMiddleware();
MyErrorMiddleware has this invoke method and MyExceptionHandlerAsync is never called for this case.
public async Task Invoke(HttpContext context)
{
try
{
//This line gets called
await _next(context);
}
catch (Exception ex)
{
//The catch block is not executed if the integer passed to the controller method overflows a 32 bit integer
//The catch block is executed if an C# WebAPI controller method is called and the controller method throws an exception
await MyExceptionHandlerAsync(context, ex);
}
}
Here is what worked best for our application.
Create base class for controller methods (code below) having a Boolean errorFlag and a List ErrorList
Derive the return objects for controller methods off the base class in #1. Each of the return classes had the error properties plus the data returned from the controller method. The data property returned for the controller would be called "ReturnData" to make the front-end JavaScript easier.
Add the model validation error handler to Startup.cs. Return the same base class as in step #1
Add in the error handling middleware catch block, code to return the error using the same error class from step #1
This handles 1) business error from controller (not a thrown exception), 2) exception thrown from controller or a method it calls, no need to have a try/catch in the controller, 3) error passing data to the WebAPI controller method (model validation error)
public class ReturnBaseClass
{
public ReturnBaseClass() { errorList = new List<string>()}
public bool errorFlag {get;set;}
public List<string> errorList {get;set;}
}
//A controller return data could look like
public class CourseDataReturn : ReturnBaseClass
{
//Always called ReturnData to make front-end JavaScript easier
public CourseInfo ReturnData {get;set;}
}
//CourseInfo is the data returned from the data repository method called from the controller.
public class CourseInfo
{
public int CourseId {get;set;}
public string CourseName {get;set;}
}
We simplified our return HTTP status codes to OK, Bad Request, Internal Server Error for back-end WebAPI calls which are made. All other HTTP status codes would be a HTTP transport error (timeout, not found (URL is bad), ....
That got us past the REST mismatch of HTTP NotFound being either the URL is not found, such as domain name does not exist, and the database row being retrieved for a HTTP GET student by student ID not finding the student.
It is not an exception. It is ModelState not valid.
So you can catch this error by:
Step 1:
Add ConfigureApiBehaviorOptions in Program.cs file:
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
});
Step 2:
Handle errors in top of Controller file:
if (!ModelState.IsValid) {
var errors = new List<string>();
foreach (var state in ModelState)
{
foreach (var error in state.Value.Errors)
{
errors.Add(error.ErrorMessage);
}
}
// Do something to handle error list
}
The code below is based on [https://briancaos.wordpress.com/2019/11/29/net-core-catch-model-binding-exceptions/]
We used a variation on this code in the solution
//In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
... //all of the configuration for services, etc...
//Add this at the very end of ConfigureServices()
//Handle errors - model bind error, bad parameter passed to WebAPI endpoint, ...
services.Configure<ApiBehaviorOptions>(options =>
options.InvalidmsResponseFactory = (ac =>
{
var ms = ac.ms;
//Properties related to errors: ms.IsValid, s.ErrorCount, ms.ValidationState
//var errors = ms.Where(x => x.Value.Errors.Count > 0);
var errs = new ValidationProblemDetails(ac.ms);
var errList = new List<string>();
//errList.Add($"Detail: {errs.Detail}");
errList.Add($"Title: {errs.Title}");
//errList.Add($"URL: {errs.Instance}");
//errList.Add($"HTTP Status Code: {errs.Status}");
//errList.Add($"Error Type: {errs.Type}");
List<string> mlist = new List<string>();
foreach (string k in errs.Errors.Keys)
{
//build one message line <KEY>: <ERROR MESSAGE 1>, <ERROR MESSAGE 2>, ...
string m = $"{k}: ";
string[] value = errs.Errors[k];
if (value.Length > 0)
{
m = $"{m}{string.Join(",", value)}";
}
mlist.Add(m);
}
string r = "";
if (msgList.Any())
{
r = string.Join(",", msgList);
}
return new BadRequestObjectResult(r);
}
));
I am not able to find how to throw exceptions as they are generated by .NET 6 Web API.
If I return BadRequest(ModelState) with added errors I am not getting same message with status, type, title etc.
By default .NET generates this kind of errors when validation error occurs:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-488f8c0057223cbba92fc1fbfc8865d8-2341d7aba29d098f-00",
"errors": {
"$": [
"The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 7 | BytePositionInLine: 0."
],
"model": [
"The model field is required."
]
}
}
I want to configure my application to respond with the same error JSON, or I want to configure so it will respond with the custom JSON fields.
I tried to add a middleware that will catch exceptions, but it does not handle model errors (which are handled by framework by itself). How can I handle errors globally, or how can I throw exceptions that will be treated the same as framework handles them? Any documentation/tutorial links are welcome!
You can disable default bad request responses like the following code:
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
So you can return any model you want in BadRequest.
However, you then have to do the model validation yourself in each endpoint like:
[HttpPost(Name = "Post1")]
public IActionResult Post1(Model1 model)
{
if (!ModelState.IsValid)
return BadRequest(new CustomApiResponse());
...
}
[HttpPost(Name = "Post2")]
public IActionResult Post2(Model2 model)
{
if (!ModelState.IsValid)
return BadRequest(new CustomApiResponse());
...
}
If you want to return a global JSON structure, you can create a filter with the ActionFilterAttribute and use it for all your endpoints so you don't need to do model validation on every endpoint.
Custom Validation Filter:
public class CustomValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new GlobalApiResponse());
}
base.OnActionExecuting(context);
}
}
You need to register your custom filter in Program.cs
builder.Services.AddControllers(options =>
{
options.Filters.Add(typeof(CustomValidationFilterAttribute));
});
This is just one of the ways you can use to achieve what you want, you can find different methods on the internet.
In .Net6 Web Api, If You Want To Validate Model By Your Code, And You Can Debug It, Just Do This:
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(new BaseResponse() { code = ResponseCode.失败, msg = "" });
}
return base.OnActionExecutionAsync(context, next);
}
}
Then Do It In The Program.cs
// Add services to the container.
builder.Services.AddControllers(options =>
{
//Add Your Filter
options.Filters.Add<ModelValidationFilterAttribute>();
}).ConfigureApiBehaviorOptions(options =>
{
//Disable The Default
options.SuppressModelStateInvalidFilter = true;
});
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?
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));
});
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."
]
}
}
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; }
}
All my models are automatically validated before hitting the endpoint, and return appropriate errors if some form of validation has failed.
I remember back in ASP.NET Core 2.2 we needed to manually call ModelState.IsValid to make sure an object has passed validation checks, but with the latest ASP.NET Core 3.0, this doesn't seem to be the case, and nowhere am I including/configuring any services explicitly for this behavior to exist.
Could someone shine some light on the matter, and perhaps link a relevant source where they mention this change?
EDIT: Is it due to the [ApiController] attribute? See: https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#automatic-http-400-responses
Thank you!
When using the [ApiController] attribute, you do not need to check ModelState.IsValid in each method since a 400 status code is automatically returned with the name of the fields where the validation failed, see https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1
You can even modify the way the 400 status code should look like. This blog article should get you started: https://coderethinked.com/customizing-automatic-http-400-error-response-in-asp-net-core-web-apis/
Add this dependency:
services.AddMvc.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomBadRequest(context);
return new BadRequestObjectResult(problems);
};
});
Your custom bad request class can look like this. Create a YourErrorClass class if you want to include additional information regarding the errors, e.g., error severity, warnings etc.
public class CustomBadRequest
{
[JsonProperty("httpstatuscode")]
public string HttpStatusCode { get; set; }
[JsonProperty("errors")]
public List<YourErrorClass> Errors { get; set; } = new List<YourErrorClass>();
public CustomBadRequest(ActionContext context)
{
this.HttpStatusCode = "400";
this.Errors = new List<YourErrorClass>();
this.ConstructErrorMessages(context);
}
private void ConstructErrorMessages(ActionContext context)
{
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
foreach (var error in errors)
{
Errors.Add(new YourErrorClass()
{
ErrorMessage = error.ErrorMessage
// add additional information, if you like
});
}
}
}
}
Yes it is because of the ApiController attribute.
However this would happen even in Core 2.2, however there was an options to set the compatibility to 2.0, this however no longer works since 3.0, which might explain the behavior you see.
To turn it off in 3.0 and up, you need to set it in the options when doing services.AddControllers() or services.AddMvc() as in the following code:
services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
}
Also note that there was a difference between core 2.1 and later versions, as described in this MSDN article, as in the following examples:
Core 2.1 sample response:
{
"": [
"A non-empty request body is required."
]
}
Core 2.2 and up sample response:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7fb5e16a-4c8f23bbfc974667.",
"errors": {
"": [
"A non-empty request body is required."
]
}
}
To get back the 2.1 behavior, you could set the compatibilty back to 2.1, but this no longer works in since 3.0 and up, instead you need the following code in the option configuration when doing services.AddControllers() or services.AddMvc() as in the following code:
services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
new BadRequestObjectResult(new SerializableError(context.ModelState));
});
I'm interested in knowing what are the best practices being followed to raise exceptions in the ODataController.
If you raise an exception in the method it is translated to response code of 500 by default and the content has details on the error. I would like to be explicit of the response code and send 400 in cases of invalid key.
For example: If the input request has an invalid key would like to return a HttpResponseCode of 400 and content should have the error details similar to raising an exception.
Thanks a lot for your inputs
OData (at least since v3) uses specific json to represent errors:
{
"error": {
"code": "A custom error code",
"message": {
"lang": "en-us",
"value": "A custom long message for the user."
},
"innererror": {
"trace": [...],
"context": {...}
}
}
}
Microsoft .Net contains Microsoft.Data.OData.ODataError and Microsoft.Data.OData.ODataInnerError classes to form OData error on a server side.
To form proper OData error response (HttpResponseMessage), that contains error details you can:
1) form and return HttpResponseMessage in controller's action using System.Web.OData.Extensions.HttpRequestMessageExtensions.CreateErrorResponse method
return Request.CreateErrorResponse(HttpStatusCode.Conflict, new ODataError { ErrorCode="...", Message="...", MessageLanguage="..." }));
2) throw HttpResponseException using the same method for creating HttpResponseMessage
throw new HttpResponseException(
Request.CreateErrorResponse(HttpStatusCode.NotFound, new ODataError { ErrorCode="...", Message="...", MessageLanguage="..." }));
3) throw custom typed exception and convert it using Web Api action filters
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is CustomException)
{
var e = (CustomException)context.Exception;
var response = context.Request.CreateErrorResponse(e.StatusCode, new ODataError
{
ErrorCode = e.StatusCodeString,
Message = e.Message,
MessageLanguage = e.MessageLanguage
});
context.Response = response;
}
else
base.OnException(context);
}
}
Use HttpResponseException,
e.g. throw new HttpResponseException(HttpStatusCode.NotFound);.
Details can be found here.
For ASP.NET Core with OData, replace the EnableQueryAttribute on your Get method with a custom attribute that catches ODataException and throws a custom exception. In most cases, this allows standard error handling to step in as expected. Originally found this solution at https://github.com/OData/WebApi/issues/1898.
For your custom attribute, use something like the following:
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions)
{
try
{
base.ValidateQuery(request, queryOptions);
}
catch (ODataException e)
{
throw new CustomException(e.Message, e){UserMessage = "Invalid OData query."};
}
}
}
On your Get method, use something like the following:
[HttpGet, CustomEnableQuery]
public virtual IQueryable<TDomainDto> Get()
{
return Repository.Get();
}
Check CreateErrorResponse in the OData documentation. The namespace to the used in Microsoft.AspNet.OData. I got my code working using this.