How can I customize the error response in Web API with .NET Core? - asp.net-core

I am using .NET Core 2.2 with Web API. I have created one class, i.e., as below:
public class NotificationRequestModel
{
[Required]
public string DeviceId { get; set; }
[Required]
public string FirebaseToken { get; set; }
[Required]
public string OS { get; set; }
public int StoreId { get; set; }
}
Using the above class I have created one method. Now I want to return a custom object, but it's returning its own object.
API method is:
public ActionResult<bool> UpdateFirebaseToken(NotificationRequestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}
var result = _notificationService.InsertOrUpdateFirebaseToken(model);
return Ok(result);
}
Here FormatOutput method is format the output.
protected Base FormatOutput(object input, int code = 0, string message = "", string[] details = null)
{
Base baseResult = new Base();
baseResult.Status = code;
baseResult.Error = message;
baseResult.TimeStamp = CommonHelper.CurrentTimeStamp;
baseResult.Code = code;
baseResult.Details = details;
baseResult.Message = message; //Enum.Parse<APIResponseMessageEnum>(code.ToString(), true); // (enum of code get value from language)
return baseResult;
}
But the issue is it returns:
{
"errors": {
"DeviceId": [
"The DeviceId field is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "80000049-0001-fc00-b63f-84710c7967bb"
}
I want to customize this error with my model. I need error message and details from return output and passed it to my model. How can I do that? I had try to debug my code and found that breakpoint on API method is not calling. So I can't handle my custom method. Is there any solution? What am I doing wrong?

When using a controller with the ApiController attribute applied, ASP.NET Core automatically handles model validation errors by returning a 400 Bad Request with ModelState as the response body. As such, your conditional testing ModelState.IsValid is essentially always false (and therefore not entered) because the only requests that will ever get this far are valid ones.
You could simply remove the ApiController attribute, but that removes a bunch of other beneficial stuff the attributes adds as well. The better option is to use a custom response factory:
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
new BadRequestObjectResult(actionContext.ModelState);
});
That's essentially what's happening by default, so you'd simply need to change the action provided there accordingly to customize it to your whims.

As Chris analyzed, your issue is caused by Automatic HTTP 400
responses.
For the quick solution, you could suppress this feature by
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
For an efficient way, you could follow the suggestion from Chris, like below:
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
//options.SuppressModelStateInvalidFilter = true;
options.InvalidModelStateResponseFactory = actionContext =>
{
var modelState = actionContext.ModelState.Values;
return new BadRequestObjectResult(FormatOutput(modelState));
};
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
And, there isn't any need to define the code below any more in your action.
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}

Related

"The JSON value could not be converted to Enum in Refit

I have one .Net Core Razor pages app which is trying to call a .Net Core API with a class library created using Refit.
I have created one Refit API interface which uses a model with enum as one of the property type.
Here is the interface snippet on the API side: IPaymentAPI interface
[Post("/recharge")]
Task<string> Recharge([Body] RechargeRequest request);
Here is the request model: The model contains one simple enum ELicenseType.
public class RechargeRequest
{
public ELicenseType LicenseType{ get; set; }
}
The ELicenseType:
public enum ELicenseType
{
NotSpecified = 0,
Standard = 1,
Commercial = 2
}
The API implementation in controller:
[HttpPost("recharge")]
public async Task<IActionResult> Recharge(
[FromBody] RechargeRequest request)
{
Recharge result = await _mediator.Send(_mapper.Map<RechargeCommand>(request));
return Ok();
}
When calling this Recharge method the Refit is throwing the ValidationApiException:
ValidationApiException: Response status code does not indicate success: 400 (Bad Request).
And the content is:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-f9178d81421bca438241dd2def43d065-edbd7f210919b24e-00",
"errors": {
"$.licenseType": [
"The JSON value could not be converted to ELicenseType. Path: $.licenseType | LineNumber: 0 | BytePositionInLine: 98."
]
}
}
It seems that the Refit library does not support Enums in the request or my JSON serializer is misconfigured.
Do you need to set this config in your startup.cs?
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddJsonOptions(opt =>
{
opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
}
When creating a Refit implementation for your API's interface, you can provide an instance of RefitSettings.
var refitSettings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
})
};
var api = RestService.For<IMyApi>(client, refitSettings);
I think that will solve your problem.

POST null because property type does not match?

I have a asp .net core API with simple REST methods like this:
[Route("api/[controller]")]
public class SomeController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Insert([FromBody] ItemClass newItem)
{
[...]
return Ok();
}
}
The model class is pretty simple as well:
public class ItemClass
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
}
When I POST this JSON to the service
{
"id": null,
"name": "Some name",
"description": "Some description",
"isActive": null
}
the method is called with newItem set to null. No error, no exception, the the object was just null. It took me quite a while to figure out the parameter isActive was the cause of the problem. In the class I defined it as bool, but JSON defined it as null. When I either change the value to true or false or when I leave it out completely or when I change the parameter to bool? it works again. The object is deserialized correctly.
I'm used to asp net core APIs to be very resilient. Usually when a parameter is simply set to its default when it can not be parsed correctly from the message.
But why does the whole object become null here?
Is there at least a way to tell asp net core to throw an exception when the deserialization fails?
When null is mapped to the bool, resulting in a type mismatch exception, it will cause the whole object become null.
If this api is 3.x, it will trigger 400. But in 2.x and 5.0, they have no exception. You can handle the null value through NewtonSoft.json.
A simple way to handel it with this configuration, it can assign a default value to bool, but it doesn't trigger exception.
In 2.x
services.AddMvc()
.AddJsonOptions(options=>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
5.0
services.AddControllers()
.AddNewtonsoftJson(option=>
{
option.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
If must return an error, you have to use a custom model binding.
public class CustomBindClassBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var itemClass = new ItemClass();
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
var body = reader.ReadToEndAsync();
var mydata = JsonConvert.DeserializeObject<JObject>(body.Result);
if(mydata["isActive"].ToString()==""|| mydata["isActive"].ToString() == null)
{
bindingContext.Result= ModelBindingResult.Failed();
throw new Exception("isActive is not correct");
}
else
{
itemClass.Id = mydata["id"].ToString();
itemClass.Name = mydata["name"].ToString();
itemClass.Description = mydata["description"].ToString();
itemClass.IsActive = bool.Parse(mydata["isActive"].ToString());
bindingContext.Result = ModelBindingResult.Success(itemClass);
}
}
return Task.CompletedTask;
}
}
In action
[HttpPost]
public async Task<IActionResult> Insert([FromBody][ModelBinder(typeof(CustomBindClassBinder))] ItemClass newItem)
{
return Ok(newItem);
}

CustomActionFilter not getting called for POST/PUT endpoint in web api [duplicate]

I am using .NET Core 2.2 with Web API. I have created one class, i.e., as below:
public class NotificationRequestModel
{
[Required]
public string DeviceId { get; set; }
[Required]
public string FirebaseToken { get; set; }
[Required]
public string OS { get; set; }
public int StoreId { get; set; }
}
Using the above class I have created one method. Now I want to return a custom object, but it's returning its own object.
API method is:
public ActionResult<bool> UpdateFirebaseToken(NotificationRequestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}
var result = _notificationService.InsertOrUpdateFirebaseToken(model);
return Ok(result);
}
Here FormatOutput method is format the output.
protected Base FormatOutput(object input, int code = 0, string message = "", string[] details = null)
{
Base baseResult = new Base();
baseResult.Status = code;
baseResult.Error = message;
baseResult.TimeStamp = CommonHelper.CurrentTimeStamp;
baseResult.Code = code;
baseResult.Details = details;
baseResult.Message = message; //Enum.Parse<APIResponseMessageEnum>(code.ToString(), true); // (enum of code get value from language)
return baseResult;
}
But the issue is it returns:
{
"errors": {
"DeviceId": [
"The DeviceId field is required."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "80000049-0001-fc00-b63f-84710c7967bb"
}
I want to customize this error with my model. I need error message and details from return output and passed it to my model. How can I do that? I had try to debug my code and found that breakpoint on API method is not calling. So I can't handle my custom method. Is there any solution? What am I doing wrong?
When using a controller with the ApiController attribute applied, ASP.NET Core automatically handles model validation errors by returning a 400 Bad Request with ModelState as the response body. As such, your conditional testing ModelState.IsValid is essentially always false (and therefore not entered) because the only requests that will ever get this far are valid ones.
You could simply remove the ApiController attribute, but that removes a bunch of other beneficial stuff the attributes adds as well. The better option is to use a custom response factory:
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
new BadRequestObjectResult(actionContext.ModelState);
});
That's essentially what's happening by default, so you'd simply need to change the action provided there accordingly to customize it to your whims.
As Chris analyzed, your issue is caused by Automatic HTTP 400
responses.
For the quick solution, you could suppress this feature by
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
options.SuppressModelStateInvalidFilter = true;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
For an efficient way, you could follow the suggestion from Chris, like below:
services.AddMvc()
.ConfigureApiBehaviorOptions(options => {
//options.SuppressModelStateInvalidFilter = true;
options.InvalidModelStateResponseFactory = actionContext =>
{
var modelState = actionContext.ModelState.Values;
return new BadRequestObjectResult(FormatOutput(modelState));
};
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
And, there isn't any need to define the code below any more in your action.
if (!ModelState.IsValid)
{
return BadRequest(FormatOutput(ModelState.Values));
}

How to validate for illegal fields in Model Validation

I have a .NET Core 2.2 web-api that accepts a PersonDto, it is getting validated with Model Validation, but it does not check for illegal fields. It only checks if matching fields are valid.
I want to make sure that the supplied JSON contains only the fields that are in my Dto (Class).
public class PersonDto
{
public string firstname { get; set; }
public string lastname { get; set; }
}
My controller looks simplified like this:
public async Task<ActionResult<Person>> Post([FromBody] PersonDto personDto)
{
// do things
}
I send it incorrect fields (name does not exist in my dto) and the ModelState is valid.
{
"name": "Diego"
}
I expected the Model Validation to complain that the field "Name" does not exist.
How can I check for illegal fields?
You could use ActionFilter and Reflection to compare the request body content to the model fields. If there are unexpected fields, manually add model errors and the ModelState.IsValid will be false.
1.Create an ActionFilter
public class CompareFieldsActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//get all fields name
var listOfFieldNames = typeof(PersonDto).GetProperties().Select(f => f.Name).ToList();
var request = context.HttpContext.Request;
request.Body.Position = 0;
using (var reader = new StreamReader(request.Body))
{
//get request body content
var bodyString = reader.ReadToEnd();
//transfer content to json
JObject json = JObject.Parse(bodyString);
//if json contains fields that do not exist in Model, add model error
foreach (JProperty property in json.Children())
{
if (!listOfFieldNames.Contains(property.Name))
{
context.ModelState.AddModelError("Filed", "Field does not exist");
}
}
}
base.OnActionExecuting(context);
}
}
2.Use the filter on your action:
[HttpPost]
[CompareFieldsActionFilter]
public async Task<ActionResult<Person>> Post([FromBody] PersonDto personDto)
{
if (ModelState.IsValid)
{
// do things
}
// do things
}

FluentValidation with IActionFilter in Asp.net Core 2.1 [duplicate]

I have a logic to apply in case the request received is a BadRequest, to do this I have created a filter:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// Apply logic
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => { options.Filters.Add<ValidateModelAttribute>(); });
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class VerifyController : ControllerBase
{
[Route("test")]
[HttpPost]
[ValidateModel]
public ActionResult<Guid> validationTest(PersonalInfo personalInfo)
{
return null;
}
}
Model:
public class PersonalInfo
{
public string FirstName { get; set; }
[RegularExpression("\\d{4}-?\\d{2}-?\\d{2}", ErrorMessage = "Date must be properly formatted according to ISO 8601")]
public string BirthDate { get; set; }
}
The thing is when I put a break point on the line:
if (!context.ModelState.IsValid)
execution reaches this line only if the request I send is valid. Why it is not passing the filter if I send a bad request?
The [ApiController] attribute that you've applied to your controller adds Automatic HTTP 400 Responses to the MVC pipeline, which means that your custom filter and action aren't executed if ModelState is invalid.
I see a few options for affecting how this works:
Remove the [ApiController] attribute
Although you can just remove the [ApiController] attribute, this would also cause the loss of some of the other features it provides, such as Binding source parameter inference.
Disable only the Automatic HTTP 400 Responses
Here's an example from the docs that shows how to disable just this feature:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// ...
options.SuppressModelStateInvalidFilter = true;
// ...
}
This code goes inside of your Startup's ConfigureServices method.
Customise the automatic response that gets generated
If you just want to provide a custom response to the caller, you can customise what gets returned. I've already described how this works in another answer, here.
An example of intersection for logging is describe in Log automatic 400 responses
Add configuration in Startup.ConfigureServices.
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();
// Perform logging here.
//E.g. logger.LogError($”{context.ModelState}”);
logger.LogWarning(context.ModelState.ModelStateErrorsToString());
// Invoke the default behavior, which produces a ValidationProblemDetails response.
// To produce a custom response, return a different implementation of IActionResult instead.
return builtInFactory(context);
};
});
public static String ModelStateErrorsToString(this ModelStateDictionary modelState)
{
IEnumerable<ModelError> allErrors = modelState.Values.SelectMany(v => v.Errors);
StringBuilder sb = new StringBuilder();
foreach (ModelError error in allErrors)
{
sb.AppendLine($"error {error.ErrorMessage} {error.Exception}");
}
return sb.ToString();
}
As the attribute filter in the life cycle of the .Net Core you can’t handle it. The filter layer with ModelState will run after the model binding.
You can handle it with .Net Core middleware as the following https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x
If you want to SuppressModelStateInvalidFilter on individual action, consider to use custom attribute suggested on https://learn.microsoft.com/en-us/answers/questions/297568/how-to-suppress-suppressmodelstateinvalidfilter-at.html. (And similar answer https://github.com/aspnet/Mvc/issues/8575)
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
private const string FilterTypeName = "ModelStateInvalidFilterFactory";
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
//if (action.Filters[i] is ModelStateInvalidFilter)
if (action.Filters[i].GetType().Name == FilterTypeName)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
Example of use
[ApiController]
public class PersonController
{
[SuppressModelStateInvalidFilter]
public ActionResult<Person> Get() => new Person();
}