Giter Site home page Giter Site logo

Comments (7)

RobTF avatar RobTF commented on August 23, 2024 1

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.

xuzhg avatar xuzhg commented on August 23, 2024

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.

RobTF avatar RobTF commented on August 23, 2024

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;

  1. 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?
  2. 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?
  3. Do all OData routes have to match something in the EDM model? Or can I simply declare new actions independently using [ODataRoute]?
  4. 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).
  5. Can you customise the routes generated for an entity? Add multiple routes etc?
  6. 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.

osky50 avatar osky50 commented on August 23, 2024

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;

  1. 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?
  2. 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?
  3. Do all OData routes have to match something in the EDM model? Or can I simply declare new actions independently using [ODataRoute]?
  4. 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).
  5. Can you customise the routes generated for an entity? Add multiple routes etc?
  6. 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.

xuzhg avatar xuzhg commented on August 23, 2024

@RobTF

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?

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?

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.

xuzhg avatar xuzhg commented on August 23, 2024

@osky50 I think your scenario should be working. Did you try to build it?

from aspnetcoreodata.

RobTF avatar RobTF commented on August 23, 2024

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 are Guid/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 base IQueryable 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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.