OData empty object with PUT/PATCH - asp.net-core

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

Related

Conflicting method/path combination for action - Swagger unable to distinguish alternate version from Route

I have the following controller setup in my solution:
[Route("api/v{VersionId}/[controller]")]
[ApiController]
[Produces("application/json")]
[Consumes("application/json")]
public class MyBaseController : ControllerBase
{
}
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class AuthenticationController : MyBaseController
{
private readonly ILoginService _loginService;
public AuthenticationController(ILoginService loginService)
{
_loginService = loginService;
}
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[HttpPost("login")]
public ActionResult<v1.JwtTokenResponse> Login([FromBody] v1.LoginRequest loginRequest)
{
var loginResult = _loginService.Login(loginRequest.Email, loginRequest.Password);
if (loginResult.StatusCode != HttpStatusCode.OK)
{
return StatusCode((int)loginResult.StatusCode);
}
var tokenResponse = new v1.JwtTokenResponse() { Token = loginResult.Token };
return Ok(tokenResponse);
}
}
Between the two versions of my API, nothing has changed for this method and so logically in my documentation I want to display that the method is still supported in the new version. Let's argue that we have a second controller of customer that has had some changed logic and hence is the reason why we have the new version 1.1 as semantic versioning dictates something new has been added but in a backwards compatible manner.
When running this code, naturally everything builds fine. The code is valid and .net core allows this sort of implementation however, when it comes to the swagger gen I am hitting issues with it producing the following error:
NotSupportedException: Conflicting method/path combination "POST api/v{VersionId}/Authentication/login" for actions - Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints),Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints). Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
As you can see above, the path is different because the version parameter passed into the route makes it that way. Furthermore, it does not make sense to create a brand new method purely to represent that the code is available through documentation so, my question is why is swagger ignoring the version differences in the path and suggesting the user of the ConflictingActionsResolver?
Furthermore, after digging into this further and seeing that a lot of other people were having the same issue (with header versioning being a particular bugbear of the community and Swaggers hard-line approach being in conflict with this) the general approach seems to be to using the conflicting actions resolver to only take the first description it comes across which would only expose version 1.0 in the api documentation and leave out the 1.1 version giving the impression in Swagger that there is no 1.1 version of the endpoint available.
Swagger UI Config
app.UseSwaggerUI(setup =>
{
setup.RoutePrefix = string.Empty;
foreach (var description in apiVersions.ApiVersionDescriptions)
{
setup.SwaggerEndpoint($"/swagger/OpenAPISpecification{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
});
How can we get around this and correctly display available endpoints in Swagger without having to create new methods that effectively result in a duplication of code just to satisfy what seems like an oversight in the Swagger spec? Any help would be greatly appreciated.
N.B. Many may suggest appending action on to the end of the route however we wish to avoid this as it would mean our endpoints are not restful where we want to strive for something like customers/1 with the GET, POST, PUT attributes deriving the CRUD operations without having to append something like customers/add_customer_1 or customers/add_customer_2 reflecting the method name in the URL.
This is my Swagger settings when using HeaderApiVersionReader.
public class SwaggerOptions
{
public string Title { get; set; }
public string JsonRoute { get; set; }
public string Description { get; set; }
public List<Version> Versions { get; set; }
public class Version
{
public string Name { get; set; }
public string UiEndpoint { get; set; }
}
}
In Startup#ConfigureServices
services.AddApiVersioning(apiVersioningOptions =>
{
apiVersioningOptions.AssumeDefaultVersionWhenUnspecified = true;
apiVersioningOptions.DefaultApiVersion = new ApiVersion(1, 0);
apiVersioningOptions.ReportApiVersions = true;
apiVersioningOptions.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swaggerGenOptions =>
{
var swaggerOptions = new SwaggerOptions();
Configuration.GetSection("Swagger").Bind(swaggerOptions);
foreach (var currentVersion in swaggerOptions.Versions)
{
swaggerGenOptions.SwaggerDoc(currentVersion.Name, new OpenApiInfo
{
Title = swaggerOptions.Title,
Version = currentVersion.Name,
Description = swaggerOptions.Description
});
}
swaggerGenOptions.DocInclusionPredicate((version, desc) =>
{
if (!desc.TryGetMethodInfo(out MethodInfo methodInfo))
{
return false;
}
var versions = methodInfo.DeclaringType.GetConstructors()
.SelectMany(constructorInfo => constructorInfo.DeclaringType.CustomAttributes
.Where(attributeData => attributeData.AttributeType == typeof(ApiVersionAttribute))
.SelectMany(attributeData => attributeData.ConstructorArguments
.Select(attributeTypedArgument => attributeTypedArgument.Value)));
return versions.Any(v => $"{v}" == version);
});
swaggerGenOptions.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"));
//... some filter settings here
});
In Startup#Configure
var swaggerOptions = new SwaggerOptions();
Configuration.GetSection("Swagger").Bind(swaggerOptions);
app.UseSwagger(option => option.RouteTemplate = swaggerOptions.JsonRoute);
app.UseSwaggerUI(option =>
{
foreach (var currentVersion in swaggerOptions.Versions)
{
option.SwaggerEndpoint(currentVersion.UiEndpoint, $"{swaggerOptions.Title} {currentVersion.Name}");
}
});
appsettings.json
{
"Swagger": {
"Title": "App title",
"JsonRoute": "swagger/{documentName}/swagger.json",
"Description": "Some text",
"Versions": [
{
"Name": "2.0",
"UiEndpoint": "/swagger/2.0/swagger.json"
},
{
"Name": "1.0",
"UiEndpoint": "/swagger/1.0/swagger.json"
}
]
}
}
There are a couple of problems.
The first issue is that the route template does not contain the route constraint. This is required when versioning by URL segment.
Therefore:
[Route("api/v{VersionId}/[controller]")]
Should be:
[Route("api/v{VersionId:apiVersion}/[controller]")]
Many examples will show using version as the route parameter name, but you can use VersionId or any other name you want.
The second problem is that you are probably creating a single OpenAPI/Swagger document. The document requires that every route template is unique. The default behavior in Swashbuckle is a document per API version. This method will produce unique paths. If you really want a single document, it is possible using URL segment versioning, but you need to expand the route templates so they produce unique paths.
Ensure your API Explorer configuration has:
services.AddVersionedApiExplorer(options => options.SubstituteApiVersionInUrl = true);
This will produce paths that expand api/v{VersionId:apiVersion}/[controller] to api/v1/Authentication and api/v1.1/Authentication respectively.

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

Asp.net Core Web Api Override Model Validation

I am creating an asp.net core web api application.
Where I am try to validate my models using fluent validation.
this is my model and validator.
public class Data
{
public string Name { get; set; }
public int Age { get; set; }
}
public class DataValidator : AbstractValidator<Data>
{
public DataValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(5);
RuleFor(x => x.Age)
.LessThan(80);
}
}
Everything works fine.
Fluent validation returns all the validations together except following case.
When my request contains following JSON, Fluent Validation doesn't get hit.
Asp.net core model validation is take place.
in that case I am getting single validation error.
{
"name": 123,
"Age" : 100
}
I got following validation message.
The JSON value could not be converted to System.String. Path
How to override above default message?
Is there any way to handle above validation in Fluent Validation?
I want both 'name' and 'age' validation messages together.
Let's look at your binding model:
public class Data
{
public string Name { get; set; }
public int Age { get; set; }
}
Here, Name is a string while Age is an int.
Your validator is just fine, otherwise you should get a compilation error when you build the app.
Now, let's look at the JSON:
{
"name": 123,
"Age" : 100
}
Instead of using name, you should use Name. Plus, the value of Name should be a string, i.e. "123" instead of 123. i.e.
{
"Name": "123",
"Age": 100
}
After that, you should be able to get the expected validation errors.

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

Localization in .NET Core 2 Web API

I've followed this guide for getting my localization to work with my validation. But either I've done something wrong, or it isn't supported.
I have a Controller:
[HttpPost]
public IActionResult Post([FromBody]Customer value)
{
if (ModelState.IsValid)
{
return Ok("Good job!");
}
return BadRequest(ModelState);
}
I register my localizations like so:
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
And my model:
public class Customer
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Here's my resx and my directory (all resx look alike):
What I want is to let the resource-file determine what message is passed for the propertyname when it's invalid.
Making this POST:
{
"firstname":"John"
}
Will currently return:
{
"LastName": [
"The LastName field is required."
]
}
Since I created a Resource for the Customer model I want the result to return this:
{
"LastName": [
"The Surname field is required."
]
}
I know I can use Display like this:
[Display(ResourceType = typeof(Models_Customer), Name = "FirstName")]
...but I thought that the entire point of naming the resource-files based on the model was to avoid it.
I have the same problem. But I guess, it works only with resx filnames like this:
Models.Customer.en-GB.resx
I tried it like you but this file name works only with culture region information.
Greeds
P.S.: I'm open for a solution like Models.Customer.en.resx