"The JSON value could not be converted to Enum in Refit - asp.net-core

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.

Related

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);
}

OData empty object with PUT/PATCH

When I am trying to send PUT or PATCH request with JSON in body I am getting object with default values.
Everything is fine with get requests. And PUT request is working if specify data as parameters in URL.
I am using .NET Core and Microsoft.AspNetCore.OData 7.5.0 NuGet package
The example:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OdataModelConfigurations : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
var product = builder.EntitySet<Product>("Products").EntityType;
product.HasKey(p => p.Id);
product.Property(p => p.Name);
}
}
[ODataRoutePrefix("Products")]
public class ProductController : ODataController
{
[ODataRoute]
[HttpPut]
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public async Task<IActionResult> Put([FromBody] Product update)
{
// some code omitted
}
}
I'v tried to use different body content and to add different headers (Specify OData-Version for example).
Here is one of body examples that I'v tried to use:
{
"#odata.context": "https://localhost:5001/odata/$metadata#Product",
"Name": "put tested",
"Id":"1"
}
Or another one:
{
"#odata.type": "#ODataAPI.Models.Product",
"Name#odata.type": "String",
"Name": "patch tested"
}
Everything works like a charm without Versioning and Swagger. Even if I am just sending simple body:
{
"Name": "put tested",
"Id":1,
"CategoryId":1
}
Was able to find next one project that shows how it is possible to combine OData and Swagger:
https://github.com/microsoft/aspnet-api-versioning/tree/master/samples/aspnetcore/SwaggerODataSample

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 can I customize the error response in Web API with .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));
}

Asp.Net core Swashbuckle set operationId

How can I set swagger operationId attribute in Asp.Net Core 2.1 project? According to this post I should use SwaggerOperationAttribute but I cannot find it in Swashbuckle.AspNetCore library. Also there is an IOperationFilter
public interface IOperationFilter
{
void Apply(Operation operation, OperationFilterContext context);
}
and I can't find any implementations for swagger generation purposes.
There are 2 other options without having to write any extra code or add extra dependency like Swashbuckle.AspNetCore.Annotations
Option 1: Convention based - SwaggerGen has an option to set CustomOperationIds. So you can simply set it to use ControllerName_HttpMethod like this:
services.AddSwaggerGen(c =>
{
c.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.HttpMethod}");
c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
});
This will add operationIds to all your methods, following ControllerName_HttpMethod convention.
Option 2: ActionFilter/Attribute based - you can configure each Action method (as you'd do with SwaggerOperation action filter by simple adding a Name property to your HTTP verb action filter like this:
[HttpPost(Name="Post_Person")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task<ActionResult<Response>> PostAsync([FromBody]Request request)
{
Response result = await _context.PostAsync(request);
return Ok(result);
}
This works exactly like [SwaggerOperation(OperationId = "Post_Person")] but without the need of EnableAnnotations
Adding a Name parameter to [HttpGet]/[HttpPost] fails with an exception in the most recent version, but putting a Name parameter on the Route attribute seems to work:
/// <summary>
/// Get all devices
/// </summary>
[Route("devices", Name = "GetAllDevices")]
[Authorize]
[HttpGet]
[Produces(typeof(Device[]))]
public async Task<IActionResult> GetAllDevices() { ...}
You can also generate the operation id based on the action name which is the method name, I found this handy when generating the API client.
services.AddSwaggerGen(c =>
{
c.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["action"]}");
c.SwaggerDoc("v1", new Info { Title = "Test API", Version = "v1" });
});
Its pretty simple
Add EnableAnnotations in ConfigureService Method
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Project HTTP API",
Version = "v1",
Description = "...."
});
options.EnableAnnotations();
});
And use in controllers
[SwaggerOperation(OperationId = "GET_API")]
You can see in this in Swagger Json as
get": {
"tags": [
"API"
],
"summary": "....",
"operationId": "GET_API"
}
You can enable annotation on swagger with the Swashbuckle.AspNetCore.Annotations NuGet package. (https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md#swashbuckleaspnetcoreannotations)
Once annotations have been enabled, you can enrich the generated Operation metadata by decorating actions with a SwaggerOperationAttribute.
[HttpPost]
[SwaggerOperation(
Summary = "Creates a new product",
Description = "Requires admin privileges",
OperationId = "CreateProduct",
Tags = new[] { "Purchase", "Products" }
)]
public IActionResult Create([FromBody]Product product)
Finally, I came to this solution:
public class SwaggerOperationNameFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
operation.OperationId = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<SwaggerOperationAttribute>()
.Select(a => a.OperationId)
.FirstOrDefault();
}
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class SwaggerOperationAttribute : Attribute
{
public SwaggerOperationAttribute(string operationId)
{
OperationId = operationId;
}
public string OperationId { get; }
}
// Startup.cs
services.AddSwaggerGen(c =>
{
...
c.OperationFilter<SwaggerOperationNameFilter>();
};
[HttpGet("{id:int}")]
[SwaggerOperation("GetById")]
public async Task<IActionResult> Get(int id)
{
...
}
But it still seems to me that I've reinvented the wheel.
add this line -
swagger.CustomOperationIds(e => $"{e.RelativePath}");
in services.AddSwaggerGen function call