FromRoute does not set Url parameters in separate DTO class - c#

When creating a new Web API project you might come up with controller endpoints expecting url params, body values and maybe queries, especially for PATCH routes.
So let's assume you would like to update your shop basket by changing the amount of a product. The endpoint expects the order id and product id from the url and the amount from the body.
[HttpPatch("{orderId}/products/{productId}")]
public async Task<IActionResult> Update(Dto dto)
{
return Ok(dto);
}
The matching Dto should hold the values from the whole request
public class Dto
{
[FromRoute]
public int OrderId { get; set; }
[FromRoute]
public int ProductId { get; set; }
[FromBody]
public int Amount { get; set; }
}
When calling the API via PATCH https://localhost:5001/orders/123/products/456 Amount is correct but both ID parameters are 0. I think they won't be set and will have their default value.
Am I missing something?

I had the same problem. This helped me.
In the controller, you need to specify [FromRoute]:
[HttpPatch("{orderId}/products/{productId}")]
public async Task<IActionResult> Update([FromRoute] Dto dto)
{
return Ok(dto);
}
In the DTO, on the fields that should be obtained from the body, you need to specify [FromBody]:
public class Dto
{
public int OrderId { get; set; }
public int ProductId { get; set; }
[FromBody]
public int Amount { get; set; }
}

Since this is a web api project, the [ApiController] attribute applies inference rules for the default data sources of action parameters. The Dto is a complex type, so it will use [FromBody] as default.
When [FromBody] is applied to a complex type parameter, any binding source attributes applied to its properties are ignored. This is why you can't get the OrderId and ProductId, the [FromRoute] attribute on them are ignored.
You can find it from the official documentation:
https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#binding-source-parameter-inference
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1#frombody-attribute
One solution is that you can receive them separately:
[HttpPatch("{orderId}/products/{productId}")]
public async Task<IActionResult> Update(Dto dto, int orderid, int productId)
{
return Ok(dto);
}

Related

.NET Core [FromBody] annotation assigns default value instead of returning 400 error if value is missing

I have created a controller method similar to
[HttpPost]
public ActionResult<MyDTO> Post([FromBody] MyDTO myDTO)
{
// do something with myDTO...
Ok();
}
and MyDTO:
namespace MyNamespace.DTO
{
public class MyDTO
{
[Required]
public int someNumber { get; set; }
[Required]
public bool someBool { get; set; }
[Required]
public DateTimeOffset someDate { get; set; }
}
}
When I send a post with a JSON body that doesn't have either defined, instead of returning a 400 or an exception of some sort, it just assigns default (falsy) values to them.
When I defined a List of another DTO class, it did return a 400 code.
How can I make the API return an error when certain required values are not provided instead of just using default values?
You have to manually control it. There's no other way.
Try something like
[HttpPost]
public ActionResult<MyDTO> Post([FromBody] MyDTO myDTO)
{
if(YourComprobation(myDto)){
BadRequest();
}
Ok();
}
Found an answer here.
I just had to make all the fields nullable and have a
if (!ModelState.IsValid)
// return 400
in the controller.

Bind query parameters to a model in ASP.NET Core

I am trying to use model binding from query parameters to an object for searching.
My search object is
[DataContract]
public class Criteria
{
[DataMember(Name = "first_name")]
public string FirstName { get; set; }
}
My controller has the following action
[Route("users")]
public class UserController : Controller
{
[HttpGet("search")]
public IActionResult Search([FromQuery] Criteria criteria)
{
...
}
}
When I call the endpoint as follows .../users/search?first_name=dave the criteria property on the controller action is null.
However, I can call the endpoint not as snake case .../users/search?firstName=dave and the criteria property contains the property value. In this case Model Binding has worked but not when I use snake_case.
How can I use snake_case with Model Binding?
You need to add [FromQuery] attribute to the model properties individually
public class Criteria
{
[FromQuery(Name = "first_name")]
public string FirstName { get; set; }
}
Solution for .net core 2.1, 2.2, 3.0 and 3.1
Or without attributes you can do something like this which is cleaner I think (of course if the model properties are same as query parameters).
Meanwhile I use it in .net core 2.1, 2.2 and 3.0 preview & 3.1.
public async Task<IActionResult> Get([FromQuery]ReportQueryModel queryModel)
{
}
For anyone that got here from search engine like me:
To make it work on asp.net core 3.1+
public async Task<IActionResult> Get([FromQuery] RequestDto request);
public class RequestDto
{
[FromQuery(Name = "otherName")]
public string Name { get; set; }
}
Will read json property otherName into RequestDto.Name so basically you have to use FromQuery in 2 places.
Above answers are IMHO too complicated for such a simple thing already provided in asp.net framework.
In my case, I had an issue where my parameter name was option and in my class I also had the property called option so it was collapsing.
public class Content
{
public string Option { get; set; }
public int Page { get; set; }
}
public async Task<IActionResult> SendContent([FromQuery] Content option)
changed the parameter to something else:
public async Task<IActionResult> SendContent([FromQuery] Content contentOptions)
According to #Carl Thomas answer, here is the easier and the strongly typed way to have snake case FromQuery name:
CustomFromQuery
public class CustomFromQueryAttribute : FromQueryAttribute
{
public CustomFromQuery(string name)
{
Name = name.ToSnakeCase();
}
}
StringExtensions
public static class ObjectExtensions
{
public static string ToSnakeCase(this string o) => Regex.Replace(o, #"(\w)([A-Z])", "$1_$2").ToLower();
}
Usage
public class Criteria
{
[CustomFromQuery(nameof(FirstName))]
public string FirstName { get; set; }
}
If the
public async Task<IActionResult> Get([FromQuery] RequestDto request);
not work for anyone, you can try [FromRoute]
public async Task<IActionResult> Get([FromRoute] RequestDto request);.
In your dto you must keep the [FromQuery]
public class RequestDto
{
[FromQuery(Name = "otherName")]
public string Name { get; set; }
}

Passing multiple Id parameter to Web Api GET or DELETE request

I have a Bussiness Entity that is recognized by 2 Keys, for example:
class UserItem {
[Key]
[Column(Order = 1)]
public string UserId {get;set;}
[Key]
[Column(Order = 2)]
public string ItemName {get; set;}
public int Count {get; set;}
}
Now using ASP.NET Web Api, how can I make an HTTP GET or HTTP DELETE to accept multiple parameters? Currently, the default generated template only accept 1 key:
class ItemController : ApiController {
.....
//api/item/[key]
[HttpGet]
[ResponseType(typeof(UserItem))]
public async Task<IHttpActionResult> GetUserItem(string id)
{
UserItem item = await db.useritems.FindAsync(id);
......
}
......
}
db is my datacontext, i'm using EntityFramework 6 with ASP.NET Web Api 2
Map your route like below will allow you to pass two parameter, you can add more that two parameter this way
[HttpGet]
[Route("api/item/{id1}/{id2}")]
[ResponseType(typeof(UserItem))]
public async Task<IHttpActionResult> GetUserItem(string id1, string id2)
{
UserItem item = await db.useritems.FindAsync(id1);
......
}
I've been able to send multiple parameters from the default WebApi setup.
Just do
Public async Task<IHttpActionResult> GetUserItem(string UserId, string ItemName)
{
UserItem item = await db.useritems.FirstOrDefault(c=>.UserId==UserId && c.ItemName==ItemName);
}
Where are you running into issues?

Using [FromUri] and [FromBody] simultaneously to bind a complex Web Api method parameter

I am using request and response models to encapsulate data that needs to be passed to methods in my ASP.NET Web Api, using [FromUri] and [FromBody] when necessary.
There are instances, however, in which I would like to use both Uri and Body to populate the properties of my request model. An example would be in updating a user, where the UserId should be passed in the Uri, but the data to update would be passed in the body content. My desired implementation would look something like this:
Model:
public class UpdateUserRequestModel
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string DisplayName { get; set; }
}
Api Method:
[HttpPut]
[Route("Update/{UserId}")]
// PUT: api/Users/Update/user#domain.net
public async Task UpdateUserAsync(UpdateUserRequestModel model)
{
// Method logic
}
I would like the property UserId to be obtained [FromUri], but the rest to be obtained [FromBody], all while keeping everything all parameters in a single object. Is this possible?
Not the cleanest solution but it should work:
[HttpPut]
[Route("Update/{UserId}")]
// PUT: api/Users/Update/user#domain.net
public async Task UpdateUserAsync(UpdateUserRequestModel model, int UserId)
{
model.UserId = UserId;
}

WebAPI OData v3 Composite Key Delete

I'm having some routing problems with OData v3, composite keys and deleting items. I've set up my controller and entities as below (stubbed the methods here, they're complete in my implementation) and can run basic queries on the data (filtering etc. for GET)
When I call the url http://localhost:62658/OData/ProductStockLimit(StockLimitGroupId=1,ProductRegexMatch=Test) with DELETE however I keep getting 404's with the message "No HTTP resource was found that matches the request URI"
I assume the routing isn't picking up this method but I have no idea why as all of my other OData routes are working correctly with deletes, the only difference I can see is that this is a composite key one.
Anyone else had this problem?
public class ProductStockLimit
{
[Key, Column(Order = 2)]
public string ProductRegexMatch { get; set; }
[Key, ForeignKey("StockLimitGroup"), Column(Order = 1)]
public int StockLimitGroupId { get; set; }
public virtual StockLimitGroup StockLimitGroup { get; set; }
[Column(Order = 3)]
public double Quantity { get; set; }
}
namespace Website.Areas.OData.Controllers
{
public class ProductStockLimitController : ODataController
{
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<ProductStockLimit> Get()
{
}
public IHttpActionResult Post(ProductStockLimit item)
{
}
public HttpResponseMessage Delete( [FromODataUri]int StockLimitGroupId,[FromODataUri] string ProductRegexMatch)
{
}
}
}
From what I've looked at, it seems the OData v3 implementation doesn't handle composite keys properly. This link has a routing convention class which when applied handles them correctly.
Quick word of caution don't use the parameter name "key" for your action method as this will cause it to try and add another "key" element in the dictionary causing an exception.

Categories