47

Attribute Routing in ASP.NET Core OData 8.0 RC

 3 years ago
source link: https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/?WT_mc_id=DOP-MVP-4025064
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Attribute Routing in ASP.NET Core OData 8.0 RC

Sam Xu

April 7th, 2021

Introduction

Attribute routing is how Web API matches the incoming HTTP requests to an action based on route template attributes decorated on controller or action. ASP.NET Core defines a set of route template attributes to enable attribute routing, such as RouteAttribute, HttpGetAttribute etc. ASP.NET Core OData 8.0 RC supports these attributes to enable you to define OData attribute routing endpoints.

In this post, I would like to share details about the changes and usages of the attribute routing in OData ASP.NET Core OData 8.0 RC. The code snippets in this post are from this sample project. Please try and let me know your thoughts.

No ODataRouteAttribute and ODataRoutePrefixAttribute

In the “history” of ASP.NET Core OData, such as 6.x and 7.x version, OData attribute routing uses two attributes to find controller and action. One is ODataRoutePrefixAttribute, the other is ODataRouteAttribute. Here’s a basic usage to define an OData attribute routing:

[ODataRoute("Books({key})")]
public IActionResult Get(int key)
{
    …
}

In ASP.NET Core OData 8.0 RC, these two attributes are gone.

Instead, OData attribute routing is changed to use ASP.NET Core route template attribute classes. ASP.NET Core has the following route template attributes:

Switch to use ASP.NET Core route attributes is straightforward. Here’s the same OData route template using HttpGetAttribute:

[ODataRoute("Books({key})")]
[HttpGet("Books({key})")]
public IActionResult Get(int key)
{
    …
}

Be noted, Head HTTP method is not supported yet in RC.

ODataRoutingAttribute

To enable OData attribute routing to work, either controller or action should decorate an attribute named ODataRoutingAttribute.

ODataRoutingAttribute is introduced in RC version to avoid polluting other ASP.NET Core route templates, since OData attribute routing is enabled by default if call AddOData().

ODataRoutingAttribute can be used on controller and action. If we decorate it on the controller, all actions in this controller are considered as OData actions. That means the attribute routing convention will parse all routing templates as OData attribute routing. For example:

[ODataRouting]
public class HandleCustomerController : Controller
{
…
}

We can decorate a specific action using ODataRoutingAttribute as:

public class HandleOthersController : Controller
{
    [ODataRouting]
    [HttpGet("odata/Orders/{key}")]
    public IActionResult Get(int key)
    {
        return Ok($"Orders{key} from OData");
    }

    [HttpGet("odata/Orders({key})")]
    public IActionResult GetOrder(int key)
    {
        return Ok($"Orders{key} from non-OData");
    }
}

Where:

  1. Get(int key) is built as OData endpoint, because it’s decorated using [ODataRouting] and the route template “Orders/{key}” is a valid OData template.
  2. GetOrder(int key) is not built as OData endpoint, it will go to normal ASP.NET Core routing.

If you run and test using following requests:

1) GET http://localhost:5000/odata/Orders/2

The response is OData payload:

{
  "@odata.context": "http://localhost:5000/odata/$metadata#Edm.String",
  "value": "Orders2 from OData"
}

2) GET http://localhost:5000/odata/Orders(3)

The response is a plain text string:

Orders3 from non-OData

ODataController

ODataController has ODataRoutingAttribute decorated as:

[ODataRouting]
public abstract class ODataController : ControllerBase
{}

So, to create your own controller and derived it from ODataController is a common way to use OData attribute routing. Here is an example:

public class HandleBookController : ODataController
{}

Starting from next section, let’s review some OData attribute routing scenarios.

Attribute routing using Http Verb attributes

The basic attribute routing scenario is to use HTTP Verb attributes directly.

Let us have the following controller as example (Be noted, we use ODataController directly):

public class HandleBookController : ODataController
{
    [EnableQuery(PageSize = 1)]
    [HttpGet("odata/Books")]
    [HttpGet("odata/Books/$count")]
    public IActionResult Get()
    {
       return Ok(_db.Books);
    }

    [EnableQuery]
    [HttpGet("odata/Books({id})")]
    [HttpGet("odata/Books/{id}")]
    public IActionResult Get(int id)
    {
        return Ok(_db.Books.FirstOrDefault(c => c.Id == id));
    }
}

In the above codes, where:

  • Each Get action contains two [HttpGet] attributes with route templates. Each [HttpGet] matches GET HTTP requests only based on the route template.
  • Each route template on the first Get() action includes string literal only, such as, “odata”, “Books” and “$count”. Particularly, “odata” is the route prefix defined in startup.cs. “Books” is OData entity set name, and “$count” is OData count segment.
  • Each route template on the second Get(int id) action includes {id} route template parameter. Therefore, the “id” value in the request is binded to int id parameter.

Based on the preceding route templates,

  • GET ~/odata/Books matches the first Get action.
  • GET ~/odata/Books(3) matches the second Get action and binds the key value 3 to the id parameter.

Attribute routing using Http Verb and Route attributes

We can decorate RouteAttribute on action and combine it with Http Verb attributes.

Let us have the following controller as example:

public class HandleBookController : ODataController
{
    [Route("odata/Books({key})")]
    [HttpPatch]
    [HttpDelete]
    public IActionResult OperationBook(int key)
    {
       // the return is just for test.
       return Ok(_db.Books.FirstOrDefault(c => c.Id == key));
    }
}

In this controller, OperationBook(int key) has two HTTP Verb attributes, [HttpPatch] and [HttpDelete]. Both have null route template. It also has a RouteAttribute with route template string. Therefore, [Route(“odata/Books({key})”)] is combined with patch and delete verb attributes to construct the following route template (From the sample, you can send “~/$odata” to get the following debug information):

Based on the route template, The Uri path Patch ~/odata/Books(2) can match this action and bind the key value 2 to the key parameter.

Attribute routing using RouteAttribute on controller

We can decorate RouteAttribute on the controller. The route template in [Route] attribute is combined with route template on the individual action in that controller. The route template of RouteAttribute is prepended before route template on the action to form the final route template for that action.

Let us have the following controller as example (be noted, I use ODataRouting on the controller):

[ODataRouting]
[Route("v{version}")]
public class HandleCustomerController : Controller
{
    [HttpGet("Customers")]
    [HttpGet("Customers/$count")]
    public IActionResult Get(string version)
    {
        return Ok(_db.Customers);
    }

    [HttpGet("Customers/{key}/Default.PlayPiano(kind={kind},name={name})")]
    [HttpGet("Customers/{key}/PlayPiano(kind={kind},name={name})")]
    public string LetUsPlayPiano(string version, int key, int kind, string name)
    {
        return $"[{data}], Customer {key} is playing Piano (kind={kind},name={name}";
    }
}

Where:

  1. HandleCustomerController has RouteAttribute, its route template string “v{version}” is prepended to route template on each individual action.
  2. Get(string version) has two [HttpGet] attributes, the route template in each [HttpGet] combines with the route template on the controller to build the following attribute routing templates:
    • ~/v{version}/Customers
    • ~/v{version}/Customers/$count
  3. LetUsPlayPiano(…) has two [HttpGet] attributes, the route template in each [HttpGet] combines with the route template on the controller to build the following attribute routing templates:
    • ~/v{version}/Customers/{key}/Default.PlayPiano(kind={kind},name={name})
    • ~/v{version}/Customers/{key}/PlayPiano(kind={kind},name={name})

Based on the attribute routing templates:

  • The URL path “GET ~/v2/Customers” matches Get(string version), where the value of version parameter is “2”.
  • The URL path “GET ~/v2/Customers/3/PlayPiano(kind=4,name=’Yep’)” matches LetUsPlayPiano(version, key, kind, name), where version=”2″, key=3, kind=4 and name=”Yep”.

Multiple RouteAttribute routes

We can also decorate multiple RouteAttribute on the controller. It means that route template of each [Route] combines with each of the route template of attributes on the action methods:

Let us have the following controller as example (be noted, I use ODataRouting on the action):

[Route("odata")]
[Route("v{version}")]
public class HandleMultipleController: Controller
{
    [ODataRouting]
    [HttpGet("orders")]
    public string Get(string version)
    {
        if (version != null)
        {
           return $"Orders from version = {version}";
        }
        else
        {
            return "Orders from odata";
        }
    }
}

So, Get(string version) has two attribute routing templates:

  • GET ~/odata/orders
  • GET ~/v{version}/orders

Based on the implementation of Get(string version), we can test it using following requests:

1) GET http://localhost:5000/odata/Orders

The response is:

{
  "@odata.context": "http://localhost:5000/odata/$metadata#Edm.String",
  "value": "Orders from odata"
}

2) GET http://localhost:5000/v2001/Orders

The response is:

{
  "@odata.context": "http://localhost:5000/v2001/$metadata#Edm.String",
  "value": "Orders from version = 2001"
}

Suppress RouteAttribute on controller

We can use “/” to suppress prepending the RouteAttribute on controller to individual action.

Let us have the following controller as example:

[Route("v{version}")]
public class HandAbolusteController: Controller
{
    [ODataRouting]
    [HttpGet("/odata/orders({key})/SendTo(lat={lat},lon={lon})")]
    public string SendTo(int key, double lat, double lon)
    {
        return $"Send Order({key}) to location at ({lat},{lon})";
    }
}

Where, SendTo(…) action has one route template as:

~/odata/orders({key})/SendTo(lat={lat},lon={lon})

Clearly, “v{version}” in [Route("v{version}")] doesn’t prepend to [HttpGet] attribute template.

Known issue: If we use two [Route(…)] on HandAbolusteController, SendTo will have two selector models associated and ASP.NET Core throws ambiguous selector exception. It’s a known issue and will fix in the next version.

Other Attributes

We can use [NonODataController] and [NonODataAction] to exclude certain controller or action out of attribute routing.

Besides, [ODataModelAttribute] has no effect to attribute routing, it’s only for the conventional routing to specify the route prefix. In attribute routing, we put the route prefix in the route template directly, either using [Route] attribute or prepend the route prefix before the route template, such as “odata” prefix in route template “odata/Books”.

We can also disable the attribute routing globally using EnableAttributeRouting property on ODataOptions.

services.AddOData(opt => opt.EnableAttributeRouting = false);

Route template parser

As mentioned in Routing in ASP.NET Core OData 8.0 Preview, OData attribute routing is also a “conventional routing”, because the template string in the attribute should follow up the OData URL convention. Here’s the definition of AttributeRoutingConvention:

public class AttributeRoutingConvention : IODataControllerActionConvention
{
    public AttributeRoutingConvention(ILogger<AttributeRoutingConvention> logger,
       IODataPathTemplateParser parser)
    { ... }

    public virtual int Order => -100;
    // … 
}

Where, IODataPathTemplateParser interface is a route template parser which is how OData parses and understands the route template string.

public interface IODataPathTemplateParser
{
    ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider);
}

IODataPathTemplateParser is registered in the service provider. It can inject into the constructor of AttributeRoutingConvention. The default route template parser uses the built-in OData Uri parser to parse the route template path. If it can’t meet your requirement, you can create your own template parser to overwrite the default one.

Summary

Attribute routing enables you to achieve more routings by constructing basic and advanced OData routing templates. Moreover, you can mix it with conventional routing to achieve more. Again, to improve OData Web API routing is never stopped. We are still looking forward to feedbacks, requirements and concerns to improve the routing design and implementation. Please do not hesitate to try and let me know your thoughts through [email protected]. Thanks.

Great thanks for Javier Calvarro Nelson and David Fowler.

Sam Xu

Senior Software Engineer, OData, Microsoft Graph, Open API, Swagger

Follow


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK