Odata Controller not working with multiple Keys - c#

I work with an ASP.NET webapi with entity framework and odataV4 controller
First i had one [Key] tag in my model and every controllerAction was shown in the swagger UI
Now i have 3 [Key] tags in my model and only controllerActions with no arguments are shown in the swagger UI
I changed my Model from
//Some other stuff
[Key]
public byte first{ get; set; }
public int second{ get; set; }
public long third{ get; set; }
//Some other stuff
to
//Some other stuff
[Key]
public byte first{ get; set; }
[Key]
public int second{ get; set; }
[Key]
public long third{ get; set; }
//Some other stuff
and the controllerAction from
public async Task<IHttpActionResult> Put([FromODataUri] byte key, Delta<lists> patch)
to
public async Task<IHttpActionResult> Put([FromODataUri] byte first, [FromODataUri] int second, [FromODataUri] long third, Delta<lists> patch)
with one key the Endpoints are shown in the swagger UI and they work, with multiple keys the Endpoints are not shown in the swagger UI and everytime i try to reach the endpoint this error shows up
"No action was found on the controller 'lists' that matches the request."
I send a PUT Request with .../myController(first=1,second=1,third=10) in the URL
What i am missing or doing wrong?

Which version do you use?
As I know, the latest 5.x and 6.x version do support the multiple keys, no matter attribute routing or convention routing you are using.
See the comments in https://github.com/OData/WebApi/blob/maintenance-V4/src/System.Web.OData/OData/Routing/Conventions/ProcedureRoutingConventionHelpers.cs#L133-L137
So for your scenario, if in convention routing, the parameter name in the method of controller should be prefixed with "key".
public async Task<IHttpActionResult> Put([FromODataUri] byte keyfirst, [FromODataUri] int keysecond, [FromODataUri] long keythird, Delta<lists> patch)
Hope it can help you.
Thanks!

I solved it creating a new EMD Class like this:
namespace ODataService.Edm
{
public static class SampleModelBuilder
{
public static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntityType<EntityTableName>().HasKey(c => new { c.first, c.second, c.third});
builder.EntitySet<EntityTableName>("EntityTableName");
return builder.GetEdmModel();
}
}
}
And then I repleace the MapODataServiceRoute in the WebApiConfig.cs to invoke my EdmModel:
config.MapODataServiceRoute("ODataRoute", null, Edm.SampleModelBuilder.GetEdmModel());
You can refer more info abot EDM here:
https://learn.microsoft.com/en-us/odata/odatalib/edm/build-basic-model
At last, I use some attribute-routing on the controller like this:
[EnableQuery]
[ODataRoute("EntityTableName({first},{second},{third})")]
public SingleResult<EntityTableName> Get([FromODataUri] byte first, [FromODataUri] int second, [FromODataUri] long third)
{
IQueryable<EntityTableName> result = dbContext.EntityTableName.Where(p => p.first == first && p.second == second && p.third == third);
return SingleResult.Create(result);
}
To extend the info about attribute routing check this link:
https://learn.microsoft.com/es-es/odata/webapi/attribute-routing
Let me know if this help you.
Good luck and great coding!

Related

Change in behavior for controller parameter binding in NET7?

I have an object as follows:
[Serializable]
[DataContract()]
public class MyObject
{
[DataMember(Order = 0)]
public int Id { get; set; }
[DataMember(Order = 1)]
public string Name { get; set; }
}
And I'm trying to post a list of objects to an API by doing:
public async void SaveAsync(IEnumerable<MyObject> items, CancellationToken ct = default)
{
var response = await client.PostAsJsonAsync(mySaveUrl, items, ct);
}
And the API endpoint is:
[ProducesResponseType(typeof(IEnumerable<DTO.MyObject>), StatusCodes.Status200OK)]
[HttpPost("SaveObjects")]
public async Task<ActionResult> SaveObjects(IEnumerable<DTO.MyObject> items)
{
await myService.SaveAsync(items);
return Ok();
}
However, when the endpoint is reached, the value of the items parameter is an empty array.
This was tried and tested code and was working for years and through many iterations of .NET.
To get the list to serialize successfully I now do need to add [FromBody]. I'm testing this directly with my WebAPI project.
What am I missing?
UPDATE
Is this a breaking change in NET7 as nowhere in my solution where I post lists of objects is working anymore. Which properties of the ApiBehaviorOptions class need to be set to get the same behavior as in NET6?
Here the list of breaking changes in NET7.

Asp.Net Core MVC - Complex Model not binding on Get Controller action

Got a mismatch somewhere between my View and Controller which is causing the latter to receive a complex object, full of null values.
[HttpGet("find")]
[ProducesResponseType(typeof(PagableResults<UserDetails>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation("FindUsers")]
public async Task<IActionResult> FindUsers([FromQuery]FindUsersSearchFilter searchFilters)
And the searchFilters object is defined like this:
public class FindUsersSearchFilter
{
public int? Page { get; set; }
public string Username { get; set; }
public string Firstname { get; set; }
public string Surname { get; set; }
}
The View is sending the data in a querystring (because it's a get method) like so:
/find?SearchFilters.Page=1&SearchFilters.Firstname=foo&SearchFilters.Surname=bar&SearchFilters.Username=
However, if you debug the controller action the breakpoint is hit but the FindUsersSearchFilter received by the method has a null value for every property.
Things I've tried:
Binding(Prefix="SearchFilters") on the controller action.
Binding("Page,Firstname,Surname,Username") on the controller action
Manually changing the URL to remove the prefix and change the capitalisation
Removing [FromQuery]
At a loss as to where to go next. Any suggestions as to what I've got wrong?
The request is wrong. It should be:
/find?Page=1&Firstname=foo&Surname=bar&Username=
When you prefix all your properties with SearchFilters the binding engine is most likely looking for a nested property like searchFilters.SearchFilters.FirstName.
So removing the prefix should make it work.
If you really need to use that syntax in the query; then create another class like this:
public class SearchFilterContainer
{
public FindUsersSearchFilter SearchFilters { get; set; } = new FindUsersSearchFilter();
}
And pass that in the action as the parameter instead like this:
[HttpGet("find")]
[ProducesResponseType(typeof(PagableResults<UserDetails>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerOperation("FindUsers")]
public async Task<IActionResult> FindUsers([FromQuery]SearchFilterContainer searchFilters)
Then inside your controller you can access the model like this searchFilters.SearchFilters.FirstName

How to pass multiple parameters to Get Method and Route them in .NET Core Web API?

I'm making a (restful) Web API in .NET Core and stumbled among some problems.
I cannot seem to find how to pass multiple subscription ID's... I need to be able to show multiple periods(invoices) of multiple subscriptions.
My route at the moment is
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices/{invoiceId:long}/categories")]
From this way it seems impossible for me to pass more subscription IDs.
Some terms I found but not fully understand are:
Model Binding
[FromQuery]
My classes:
public class Subscription
{
public string Name { get; set; }
public long TenantId { get; set; }
public string Guid { get; set; }
}
public class Invoice
{
public long SubscriptionId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public long PortalId { get; set; }
}
My controllers with routes [Route("tenants/{tenantId:long}/subscriptions")] and [Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<SubscriptionViewModel> Find(long tenantId)
{
var subscriptionList = _subscriptionManager.Find(tenantId);
...
return subscriptionViewModels;
}
[HttpGet]
public IEnumerable<InvoiceViewModel> Find(long subscriptionId)
{
var invoiceList = _invoiceManager.Find(subscriptionId);
...
return invoiceViewModels;
}
Please note that i'm using a Mapper for my data (which is why i'm using ViewModels).
The currently written code is for a specific subscription.
I am looking for a Route like /api/invoices?subscriptionId=x,y,z
I understand(?) I need the [FromQuery] for that, but I cannot seem to find out how, especially if my parameter (subscriptionId) stays the same.
for the requirement which you have mentioned as:
I am looking for a Route like /api/invoices?subscriptionId=x,y,z
You can do couple of things:
pass the subscriptionIds one after the other separated by & in the query string of the URL and change the input parameter of action method to accept array of subscriptionIds
example of route:
/api/invoices/find?subscriptionId=x&subscriptionId=y&subscriptionId=z
example of action method parameter accepting array of subscriptionIds:
public IEnumerable<InvoiceViewModel> Find([FromQuery]long[] subscriptionId)
pass the comma separated string as querystring in the URL and write a piece of logic in the action method to split the string based on comma to get an array of subscriptionIds
example of route:
/api/invoices/find?subscriptionIds=x,y,z
example of action method:
public IEnumerable<InvoiceViewModel> Find([FromQuery]string subscriptionIds)
{
var ids = subscriptionIds.Split(',').Select(int.Parse).ToArray();
// do the logic on multiple subscriptionIds
}
Apart from this, you can go for creating custom model binders as well as suggested in other answers.
Hope this helps.
There can be many ways to achieve this task (I can think of two-three for now).
1) instead of long subscriptionid take a string as an input and validate it before proceeding further.
[HttpGet]
public IEnumerable<InvoiceViewModel> Find(string subscriptionIds)
{
var list = validateInput(subscriptionIds);
var invoiceList = _invoiceManager.FindList(list);
...
return invoiceViewModels;
}
public IList<long> validateInput(string subscriptionIds)
{
var list = subscriptionIds.Split(",");
... // Code to convert each element in long and throw if it is not long
return longlist;
}
2) Create custom model binders.
Steps are mentioned here:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
=> [FromUri] attribute can be used to bind the Complex types from query string parameters but i am not sure how i would use that.
If you ask me, i would go for approach-1 (not to increase complexity).
You can create a specific Request view model which accepts a collection of invoice ids:
public class InvoiceRequestModel
{
IEnumerable<long> InvoiceIDS { get; set; }
}
and use it for your action method:
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<InvoiceViewModel> Get(InvoiceRequestModel requestModel)
{
}
In the case you want to use query parameters, mark your action parameter with the [FromQuery] attribute:
[Route("tenants/{tenantId:long}/subscriptions/{subscriptionId:long}/invoices")]
[HttpGet]
public IEnumerable<InvoiceViewModel> Get([FromQuery]IEnumerable<long> invoiceIDs)
{
}
and on creating the request, pass each value with the same key in the query string:
invoiceIDs=1&invoiceIDs=2&invoiceIDs=3
Finally, it will look like this:
tenants/{tenantId}/subscriptions/{subscriptionId}/invoices?invoiceIDs=1&invoiceIDs=2&invoiceIDs=3

return decimal value in web api

I have following method in repository project , and I'm trying to get that value via web api,
Method
public decimal findBookPrice(int book_id)
{
var bookprice = (
from r in context.Books
where r.Book_Id == book_id
select r.Price
).FirstOrDefault();
return bookprice;
}
Book Class
public class Book
{
[Key]
public int Book_Id { get; set; }
[Required]
public string Book_Title { get; set; }
[DataType("decimal(16 ,3")]
public decimal Price { get; set; }
...
}
}
Web API method
// GET: api/BookPrice/3
[ResponseType(typeof(decimal))]
public IHttpActionResult GetBooksPriceById(int id)
{
decimal bookprice = db.findBookPrice(id);
return Ok(bookprice);
}
but once I direct to url which is http://localhost:13793/api/BookPrice/2
I'm getting following output not the decimal value
The shown error message is caused by a routing problem. The ASP.NET MVC framework was not able to find the right controller or action for the URL
http://localhost:13793/api/BookPrice/2
The default routing rule in ASP.NET MVC takes BookPriceand tries to find the BookPriceController. As you stated in your comment, the action is in a BooksWithAuthersController. Therefore the URL has to be (if you want to use the default routing rule):
http://localhost:13793/api/BooksWithAuthers/2
Have a look at article if you want to read more about this topic.
EDIT:
Looking at the whole controller code you will find the two action methods called GetBooksWithAuthersById and GetBooksPriceById. Because both start with get and have got the same parameter list (int id), the ASP.NET MVC framework has got two possible action methods for the URL /api/BooksWithAuthors/2. To solve this ambiguity you can give the GetBooksPriceById action a separate route via the [Route] annotation.
Like in this slightly adjusted BooksWithAuthersController:
public class BooksWithAuthersController : ApiController
{
[ResponseType(typeof(BookWithAuther))]
public IHttpActionResult GetBooksWithAuthersById(int id)
{
...
}
[ResponseType(typeof(decimal))]
[Route("api/bookswithauthers/{id}/price")]
public IHttpActionResult GetBooksPriceById(int id)
{
...
}
}
In order get the price of a book, the URL http://localhost:13793/api/BooksWithAuthers/2/price will return the decimal value.

Model [FromBody] and Guid mapping in Web Api 2

I'm implementing web api 2 and I found strange behaviour of the Guid mapping. Here is my problem definition
This is my model example
public class MyModel
{
public Guid Id { get; set; }
public string Name { get; set; }
}
I have following action on my controller where I have MyModel as input [FromBody]
[HttpPost, Route("create")]
public IHttpActionResult Create([FromBody]MyModel model)
{
// some implementation
}
Everything works fine instead of Guid mapping. When I post JSON of a new MyModel in the request body :
{
"Id":"1d93dfa2-sb34-403d-a766-bdcf1cf47a71",
"Name":"name"
}
Name is set correctly as "name" but the Guid is every time generated as a new Guid.
What can cause this issue please ? How can I set correct mapping of the Guid value?
The problem is that 1d93dfa2-sb34-403d-a766-bdcf1cf47a71 is not a valid Guid.
Guids only contain 0-9 and a-f, whereas your string has an s in it and is therefore not a valid Guid so the Model Binder does not bind anything to that property in your model.

Categories

Resources