Comments (7)
Ok, I think I'm making progress slowly. I seem to have managed to achieve some of the stuff I was after, but I have some observations/questions. I have been making extensive use of that route dumping middleware to see how the library reacts to various attempts.
(See project at https://github.com/RobTF/AspNetCoreODataExamples/tree/master/AttributeRouting)
Initial setup
When adding an entity set to an EDM model
public static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Account>("Accounts");
return builder.GetEdmModel();
}
It appears that the OData library searches for a controller with the same name (e.g. AccountsController
- case sensitive!) and if it sees a Get
method, adds two routes; ~/Accounts
and ~/Accounts/$count
. The user can however, access these routes in a case insensitive way (e.g. /accounts
works as well as /Accounts
). So the case sensitivity seems to only be in the controller/action wire up process - is this expected? I had assumed that the entity set name would only affect the routes, but the routes seem case insensitive anyway.
Supporting /$count
I need to be able to make adjustments to the query in my controller so my action looks like this;
public IActionResult Get(ODataQueryOptions<Account> options)
{
var query = _accountRepository.Get();
var finalQuery = options.ApplyTo(query.ProjectTo<Account>(_mapper.ConfigurationProvider)) as IQueryable<dynamic>;
return Ok(finalQuery);
}
However, performing a GET against ~/accounts/$count
throws an exception, I'm guessing this works with [EnableQuery]
but I don't believe I can use this for my specific scenario. I have managed to work around this using the following
public IActionResult Get(ODataQueryOptions<Account> options)
{
var countOnly = options.Count?.Value == true && options.RawValues.Count == null;
var query = _accountRepository.Get();
var finalQuery = options.ApplyTo(query.ProjectTo<Account>(_mapper.ConfigurationProvider)) as IQueryable<dynamic>;
return Ok(countOnly ? finalQuery.Count() : finalQuery);
}
Does this make sense?
Enabling the "key" endpoint (e.g. /Accounts({key})
)
This was confusing as I had expected it to just appear automatically in the routing table, but maybe 404 if there was no matching controller/action.
The logic here seems to be that the OData library hunts for an action named Get
with a parameter of key
. To answer one of my previous questions, it appears that a Guid
key is supported. What got me is that I originally had the method looking like this;
public async Task<IActionResult> GetById(Guid id, ODataQueryOptions<Account> options)
{
var query = _accountRepository.Get().Where(x => x.Id == id);
var finalQuery = options.ApplyTo(query.ProjectTo<Account>(_mapper.ConfigurationProvider)) as IQueryable<dynamic>;
var item = await finalQuery.FirstOrDefaultAsync();
return item == null ? NotFound() : Ok(item);
}
This just silently fails until you "tune" the naming convention of the controller (both method name and parameter name). This didn't seem to be explicitly detailed anywhere, so I had to tune the code in my repo to get closer to the example until I found the magic that made the routes appear. The parameter name does not appear to react to the name of the designated key property of the entity type, I'm guessing the term "key" is an OData term hence the insistence on this name?
I also note that it automatically allows both ~/Accounts({key})
as well as ~/Accounts/{key}
. I did not realise this until I saw the routes pop in.
Confusion with [HttpGet]
It appears that the straightforward Get
methods don't need the [HttpGet]
attribute applying, but methods representing actions and functions do. This caught me out as I had understood from previous conversations that attributes such as [HttpGet]
were not supported by the OData library and were an MVC/WebAPI concern - apparently there is more to it? What are the rules?
EdmModel based routing vs. Attribute Routing
This is still a mystery to me; I have the following function in my EDM model
builder.EntitySet<Account>("Accounts")
.EntityType.Function("GetUsers")
.ReturnsCollectionFromEntitySet<User>("Users");
And I found a winning combination of action name and parameters;
[HttpGet]
public IActionResult GetUsers(Guid key, ODataQueryOptions<User> options)
{
var groupName = User.Claims.First(c => c.Type == "api.group").Value;
var query =
from u in _userRepository.GetUsersByAccount(key)
where u.GroupName == groupName
select u;
var finalQuery = options.ApplyTo(query.ProjectTo<User>(_mapper.ConfigurationProvider)) as IQueryable<dynamic>;
return Ok(finalQuery);
}
This made the OData library spit out some new routes which seem to work
Controller | Action | HttpMethods | Templates |
---|---|---|---|
ODataExample.Controllers.AccountsController |
IActionResult GetUsers(Guid,ODataQueryOptions``1) |
GET | ~/Accounts({key})/Default.GetUsers() |
ODataExample.Controllers.AccountsController |
IActionResult GetUsers(Guid,ODataQueryOptions``1) |
GET | ~/Accounts({key})/GetUsers() |
ODataExample.Controllers.AccountsController |
IActionResult GetUsers(Guid,ODataQueryOptions``1) |
GET | ~/Accounts/{key}/Default.GetUsers() |
ODataExample.Controllers.AccountsController |
IActionResult GetUsers(Guid,ODataQueryOptions``1) |
GET | ~/Accounts/{key}/GetUsers() |
I attempted to use the OData attribute routing stuff to change the route:
[HttpGet]
[ODataRoute("Accounts/{key}/SomethingElse()", prefix: "")] // Added OData route attribute
public IActionResult GetUsers(Guid key, ODataQueryOptions<User> options)
{
var groupName = User.Claims.First(c => c.Type == "api.group").Value;
var query =
from u in _userRepository.GetUsersByAccount(key)
where u.GroupName == groupName
select u;
var finalQuery = options.ApplyTo(query.ProjectTo<User>(_mapper.ConfigurationProvider)) as IQueryable<dynamic>;
return Ok(finalQuery);
}
But I simply get the following error in the console;
warn: Microsoft.AspNetCore.OData.Routing.Conventions.AttributeRoutingConvention[0]
The path template 'Accounts/{key}/SomethingElse()' on the action 'GetUsers' in controller 'Accounts' is not a valid OData path template. Resource not found for the segment 'SomethingElse'.
Is there any more information on how attribute routing works? The EDM model seems to just be dictating the routes at the moment.
Thanks for the patience with all these questions. Hopefully this will help others new to the library as well?
from aspnetcoreodata.
Basically, OData routing is built upon the ASP.NET Core endpoint routing.
Not matter OData convention routing or OData attribute routing in 8.0, see here, all are used to build the "route template" and associate that template to an action in the controller.
The only different is that the "route template" should construct based on the Edm model and the OData URI convention.
So far, we have to introduce the ODataRoutePrefixAttribute and ODataRouteAttribute to help us construct the "route template".
Therefore, if you only use "HttpGet", it's not used in the OData routing. Hope, later, we can figure out the design to use "HttpGet" to set the OData attribute routing.
from aspnetcoreodata.
Hi,
Thanks for the response! I think I understand the issue somewhat now - are you saying that attributes like [HttpGet] are used by ASP.NET MVC Core to define its routing templates, but as the OData library exists effectively outside of ASP.NET MVC Core (or at least as a sort of extension of this) it has to build its own additional routing templates.
So as I understand it, the OData library works something like this;
Firstly, we start with a basic Web API;
Verb | Route | Controller/Action | Handled By |
---|---|---|---|
GET | /api/links | LinksController.Get() | MVC |
GET | /something/else | SomethingController.Else() | MVC |
We want to add ODATA, so you effectively do an AddOData()
call, and there are two components related to routing, the prefix, and the EDM model.
So if our model was, created with the following;
public static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Account>("Accounts");
builder.EntitySet<User>("Users");
return builder.GetEdmModel();
}
We glue this into the routing using something like;
services.AddOData(opt => opt.AddModel("odata", EdmModelBuilder.GetEdmModel()));
This then extends the routing table, building a set of routes using "odata" as the root (as this was specified as the "prefix"), attaching the routes pertaining to the given model underneath it. This gives us the following extended routing table;
Verb | Route | Controller/Action | Handled By |
---|---|---|---|
GET | /api/links | LinksController.Get() | MVC |
GET | /something/else | SomethingController.Else() | MVC |
GET | /odata/users | UsersController.Get() | OData |
GET | /odata/accounts | AccountsController.Get() | OData |
As I have come to understand it, this is the purpose of the "prefix" concept in the OData routes, as otherwise you would end up with a mish-mash of MVC and OData routes at the same level, which may be undesirable but which is possible anyway by simply not giving a prefix if required.
If my assumptions above are correct, I therefore have a few immediate questions;
- If I specify an entity set in my EDM model, what routes does this create by default? (i.e. is a route such as
/users({id}
) automatically added? I guess more can be added using things like Functions in the Edm model. Is there a tool for dumping all current OData route templates? - Having OData actions live alongside "normal" non OData Web API actions is often desirable - is there any specific guidance on how to approach this? It seems to work to some degree if I simply have no prefix on the OData model, is this a supported scenario?
- Do all OData routes have to match something in the EDM model? Or can I simply declare new actions independently using
[ODataRoute]
? - Is it possible to create a "drill down" type API (e.g.
accounts/{accountId}/users
) without using navigation properties (desirable as the API logic may need to control the query). - Can you customise the routes generated for an entity? Add multiple routes etc?
- Can the documentation on routing be updated to make the above clearer? Currently it seems that the information makes assumptions that the reader has some knowledge of how this works already, but this isn't always the case.
Ultimately I want to simply be able to write an ASP.NET Web API, where some GET endpoints allow me to add OData querying functionality so clients can perform projections/paging etc., where I can query some data source such as EF and return DTO objects (or a projection) representing the requested entities, as without writing reams of bespoke code this isn't possible without the OData library. This may ultimately mean having the OData library be a bit more "light touch" or allowing developers to opt into specific parts of the spec to make things easier.
Looking online I don't think the above is an uncommon goal, and it seems likely that the querying capability alone is probably what 80%+ of developers are ultimately after, as it is much easier to roll the POST/PUT/PATCH methods ourselves without having help from the OData library, whereas there is no alternative for all the juicy features of the GET queries.
Anything that can be done to make this clearer would be much appreciated!
from aspnetcoreodata.
Hi,
Thanks for the response! I think I understand the issue somewhat now - are you saying that attributes like [HttpGet] are used by ASP.NET MVC Core to define its routing templates, but as the OData library exists effectively outside of ASP.NET MVC Core (or at least as a sort of extension of this) it has to build its own additional routing templates.
So as I understand it, the OData library works something like this;
Firstly, we start with a basic Web API;
Verb Route Controller/Action Handled By
GET /api/links LinksController.Get() MVC
GET /something/else SomethingController.Else() MVC
We want to add ODATA, so you effectively do anAddOData()
call, and there are two components related to routing, the prefix, and the EDM model.So if our model was, created with the following;
public static IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder(); builder.EntitySet<Account>("Accounts"); builder.EntitySet<User>("Users"); return builder.GetEdmModel(); }
We glue this into the routing using something like;
services.AddOData(opt => opt.AddModel("odata", EdmModelBuilder.GetEdmModel()));
This then extends the routing table, building a set of routes using "odata" as the root (as this was specified as the "prefix"), attaching the routes pertaining to the given model underneath it. This gives us the following extended routing table;
Verb Route Controller/Action Handled By
GET /api/links LinksController.Get() MVC
GET /something/else SomethingController.Else() MVC
GET /odata/users UsersController.Get() OData
GET /odata/accounts AccountsController.Get() OData
As I have come to understand it, this is the purpose of the "prefix" concept in the OData routes, as otherwise you would end up with a mish-mash of MVC and OData routes at the same level, which may be undesirable but which is possible anyway by simply not giving a prefix if required.If my assumptions above are correct, I therefore have a few immediate questions;
- If I specify an entity set in my EDM model, what routes does this create by default? (i.e. is a route such as
/users({id}
) automatically added? I guess more can be added using things like Functions in the Edm model. Is there a tool for dumping all current OData route templates?- Having OData actions live alongside "normal" non OData Web API actions is often desirable - is there any specific guidance on how to approach this? It seems to work to some degree if I simply have no prefix on the OData model, is this a supported scenario?
- Do all OData routes have to match something in the EDM model? Or can I simply declare new actions independently using
[ODataRoute]
?- Is it possible to create a "drill down" type API (e.g.
accounts/{accountId}/users
) without using navigation properties (desirable as the API logic may need to control the query).- Can you customise the routes generated for an entity? Add multiple routes etc?
- Can the documentation on routing be updated to make the above clearer? Currently it seems that the information makes assumptions that the reader has some knowledge of how this works already, but this isn't always the case.
Ultimately I want to simply be able to write an ASP.NET Web API, where some GET endpoints allow me to add OData querying functionality so clients can perform projections/paging etc., where I can query some data source such as EF and return DTO objects (or a projection) representing the requested entities, as without writing reams of bespoke code this isn't possible without the OData library. This may ultimately mean having the OData library be a bit more "light touch" or allowing developers to opt into specific parts of the spec to make things easier.
Looking online I don't think the above is an uncommon goal, and it seems likely that the querying capability alone is probably what 80%+ of developers are ultimately after, as it is much easier to roll the POST/PUT/PATCH methods ourselves without having help from the OData library, whereas there is no alternative for all the juicy features of the GET queries.
Anything that can be done to make this clearer would be much appreciated!
I support this request 100%. How this is described by RobTF it is similar to what I had working with OData 7.4. With OData 8 I lost all that flexibility.
With Odata 7.4, Configuration like this:
services.AddOData();
...........
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.Select().Filter().Expand().OrderBy().Count().MaxTop(10);
endpoints.EnableDependencyInjection();
endpoints.MapODataRoute("odata", "odata", KatanaOdata.GetEdmModel());
});
And a Controller like this:
[Route("api/[controller]")]
[ApiController]
public class FaxesController : ODataController
{
private readonly ICrudService<Fax> _faxesService;
public FaxesController(
ICrudService<Fax> crudService,
)
{
_faxesService = crudService;
}
[HttpGet]
[EnableQuery()]
public async Task<IQueryable<Fax>> GetFaxes() => await _faxesService.GetAsQueryable();
[HttpGet("{id}")]
[EnableQuery()]
[ODataRoute("faxes({id})")]
public async Task<IQueryable<Fax>> GetFax([FromODataUri] int id) => await _faxesService.GetAsQueryable(id);
.....
}
I was able to consume my API GET using either:
/api/faxes
or
/odata/faxes //diferent response format, which is ok.
I was also able to do the API Get by ID in either format:
/api/faxes/234
or
/odata/faxes(234)?$select=faxName
Anything that can be done will be really appreciated.
from aspnetcoreodata.
First, your assumption about extended routing table is right, except that the request is case sensitive for the entity set name.
GET | /odata/Users | UsersController.Get() | OData |
---|---|---|---|
GET | /odata/Accounts | AccountsController.Get() | OData |
If I specify an entity set in my EDM model, what routes does this create by default? (i.e. is a route such as /users({id}) automatically added? I guess more can be added using things like Functions in the Edm model. Is there a tool for dumping all current OData route templates?
- I provided a "$odata" endpoint for my https://github.com/OData/AspNetCoreOData/tree/master/sample/ODataRoutingSample
You can send "~/$odata" to see all OData route templates.
Having OData actions live alongside "normal" non OData Web API actions is often desirable - is there any specific guidance on how to approach this? It seems to work to some degree if I simply have no prefix on the OData model, is this a supported scenario?
- non-prefix is also supported. Call "AddModel" without the prefix. For the action questions, would you please share more detail requirement for me?
Do all OData routes have to match something in the EDM model? Or can I simply declare new actions independently using [ODataRoute]?
- Yes, OData routes, as its name mentioned, should match something in the Edm model. The route template in [ODataRoute] should be a valid odata path. However, you can customize it using own routing convention and path translator. I have tried it yet.
Is it possible to create a "drill down" type API (e.g. accounts/{accountId}/users) without using navigation properties (desirable as the API logic may need to control the query).
- You don't need to do any special, just follow up the convention to create the "action" in the controller to handle the request. as your example, you can create 'IActionResult GetUsers(int key)` will automatically handle the request.
Can you customise the routes generated for an entity? Add multiple routes etc?
- My example has a lot of routing example. Can you take a look?
Can the documentation on routing be updated to make the above clearer? Currently it seems that the information makes assumptions that the reader has some knowledge of how this works already, but this isn't always the case.
- Yes. Thanks for your suggestion. I am trying my best to doc more, you can find the related blogs. Please be patient that there's only one (me) working on this part.
I do hope get more feedbacks from your real implementation. Don't hesitate involve me into your project design and implementation.
from aspnetcoreodata.
@osky50 I think your scenario should be working. Did you try to build it?
from aspnetcoreodata.
Hi,
Thanks again for the response; just a few items.
- All the examples of the keys endpoints (e.g.
Products(key)
) use integer keys. Does this work if my keys areGuid
/uniqueidentifier
? - I copied the route dumping code from the routing example, but it never generates or lists a key endpoint - how do I activate this?
- Would it be possible to see an example whereby I can drill down (e.g.
accounts/{accountId}/users/
) and explicitly control the baseIQueryable
used at each level of the drill down so I can run permissions checks etc. - What happens if I specify an odata prefix in the Startup.cs and also annotate the controller? What is the relationship? Can I control everything using attributes?
- Are there any examples of creating normal Web API controllers where only certain actions are OData actions, and both routing paradigms (Web API and OData) live together in one controller?
I'm keen on using OData but ideally I don't want all of my API actions being sucked out of the normal MVC/Web API way of working into an alternate OData "dimension" with alternative routing mechanics, and where everything needs to be double-specified (both in the routes and in an EDM model), so I'd like to keep as many endpoints away from OData as possible.
I keep going over the blog posts and examples but everything feels like I'm being started at step 5 as opposed to step 1. The examples you've provided are great, but I suppose the issue with documentation by examples is you only cover the items shown in the examples.
thanks!
from aspnetcoreodata.
Related Issues (20)
- $select-ing non-key properties inside $expand of contained entity with odata.metadata=full fails despite being autoselected HOT 2
- Proposal: Break out OData Parsing and decouple from EF Core HOT 6
- Disabling Automatic Expansion in Nested OData Entity Objects HOT 5
- Annotation to handle sanitization of field names in open types HOT 3
- Inefficient Pagination Handling in Expanded Entities ($expand ) in Microsoft.AspNetCore.OData HOT 6
- Navigation properties are not serialized in operation results HOT 7
- Error reading multipart Batch Response HOT 6
- typo odaya
- odata.metadata=full throws exceptions with $apply HOT 1
- Getting System.UriFormatException: Invalid URI: The Uri string is too long. even when using /$query
- AutoExpand and AutoSelect not working correctly with $Expand and $Select uri query parameters
- Invalid $filter and $orderby cause error 500 while it is actually bad request HOT 1
- $select doesn't work on fields of type IList<string> HOT 2
- Can't select complex property HOT 5
- OData v8.2.5: Selecting and Filtering Complex Dictionary Properties HOT 4
- $expand not working with ODataQueryOptions in the backend HOT 2
- OData filtering doesn't work with polymorphic entities HOT 3
- Really long POST $query results in incomplete JSON response HOT 5
- ODataException when entity has DateOnly or TimeOnly member HOT 6
- Error in combination with EF and SplitToTable HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from aspnetcoreodata.