ASP.NET Core OData 8.0 Preview for .NET 5

Sam Xu

Sam

Recently, OData team released the 8.0.0 preview version of ASP.NET Core OData on nuget.org. It is the first version of OData supporting .NET 5, especially for ASP.NET Core 5. This version includes some breaking changes, such as model builder separation, namespace changes and configuration changes etc. Most importantly, the routing mechanism is changed a lot comparing to the previous 7.x version.

This post is intended to introduce such changes using a basic tutorial about how to build OData service through ASP.NET Core OData 8.0 preview package. Besides, we are looking for requirements of features and reports of any bugs, issues that we can fix before the general availability. Please don’t hesitate to file any kind of issues at ASP.NET Core OData Github Repo.

Let us get started.

OData Service in practical

The basic flow to create OData service using ASP.NET Core OData 8.0 preview is same as the flow introduced in ASP.NET Core OData now Available. Please refer to that post for other details not listed in this post.

Create application

Visual Studio 2019 preview is necessary to create the ASP.NET Core 5.0 web application. So, let’s select “ASP.NET Core Web Application” project template in the following dialog to create the skeleton of the ASP.NET Core OData service.

In the following “Create a new ASP.NET Core web application” dialog, select “API” and un-check the “Configure for HTTPs” for simplicity to create the application. Be noticed, ASP.NET Core 5.0 is selected as the target platform.

As mentioned, ASP.NET Core OData 8.0 is a Nuget package. Let’s use Nuget Package Manager to install it. In the below dialog, use “nuget.org” as package source, search and select “Microsoft.AspNetCore.OData” package, check “Include prerelease” and select 8.0.0-preview version to install.

EF Core is also used in this post, so do the same process as above to install “Microsoft.EntityFrameworkCore.InMemory” (so far, it’s 5.0.0-rc.2) and its dependencies (for simplicity, we use In Memory data source also).

Build Entity Data Model

We re-use the C# class models in Add the Model class section of ASP.NET Core OData now Available to build the Entity Data Model (EDM).

We can copy/paste the “GetEdmModel()” method at Build the Edm Model into “Startup” class in the new project.

Be noted, the ASP.NET Core OData 8 preview depends on an individual OData model builder nuget package. The individual OData model builder has the same functionality as it in the existing Web API OData except the namespace. Please find more details about the OData model builder from OData Model Builder now Available.

Register the OData Services

There is a big configuration change comparing to the previous ASP.NET Core OData version. In the previous 7.5 Web API OData version, we have “AddOData()” to register the mandatory services ahead to provide OData functionality and “MapODataRoute(…)” to register the single OData route based on the given OData Edm model. For example:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRouting();
        services.AddOData();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapODataRoute("odata", "odata", GetEdmModel());
        });
    }

    private static IEdmModel GetEdmModel()
    {
        // …
    }
}

In the 8.0 preview, the configuration is changed. Where, “AddOData()” is kept, meanwhile “MapODataRoute(…)” is gone. “AddOData()” accepts a delegate to do the configuration on ODataOptions. For example, We can register Edm model, setup the query configuration by calling methods on ODataOptions. Below is the new configuration in 8.0 preview:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists"));
        services.AddControllers();
        services.AddOData(opt => opt.AddModel("odata", GetEdmModel()));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

    private static IEdmModel GetEdmModel()
    {
        // …
    }
}

Run and test

Let’s re-use the “BooksController” and “PressesController” from the ASP.NET Core OData now Available and put them into the project. Now, the OData service is ready to test.

For example, we can query a single book as by sending HTTP GET request at “http://localhost:5000/odata/Books(1)“, the response has the following payload:

{
  "@odata.context": "http://localhost:5000/odata/$metadata#Books/$entity",
  "Id": 1,
  "ISBN": "978-0-321-87758-1",
  "Title": "Essential C#5.0",
  "Author": "Mark Michaelis",
  "Price": 59.99,
  "Location": {
    "City": "Redmond",
    "Street": "156TH AVE NE"
  }
}

That’s it.

Multiple models

It’s easy to config multiple models in one OData service. Below is the sample codes that configures two models using different prefixes.

IEdmModel model1 = GetEdmModel1();
IEdmModel model2 = GetEdmModel2();
services.AddOData(opt => opt.AddModel("v1", model1).AddModel("v2", model2));

Where, “v1” and “v2” are the prefixes used before the OData path. It should be unique in one OData service.

In this configuration, we can call the service using following request Uri:

  • GET http://localhost:5000/v1/Books(1)” or
  • GET http://localhost:5000/v2/Books(1)”

There’s another overload of “AddModel()” method without “prefix” parameter as below. It means there’s no prefix before the OData path.

services.AddOData(opt => opt.AddModel(model));

The request Uri should look like:GET http://localhost:5000/Books(1)

Prefix template

The “prefix” parameter in the method “AddModel” could be a template. For example

services.AddOData(opt => opt.AddModel("v{version}", model));

In the case, we can call OData service using different version. for example:

  • http://localhost:5000/v1/Books(1) #1
  • http://localhost:5000/vbeta/Books(1) #2

Where, the version in #1 is “1”, meanwhile the version in #2 is “beta”.

You can inject a “version” parameter in the action of the controller to retrieve the “version” string. for example:

public IActionResult Get(int key, string version)
{
    // do something
}

where, version has “1” for #1 request and “beta” for #2 request.

$batch

For performance, $batch is disabled by default. In order to enable $batch, you should do following configurations:

1. Config the model using a batch handler. That is, you can call the following overload version to set up a batch handler.

public ODataOptions AddModel(string prefix, IEdmModel model, ODataBatchHandler batchHandler)

2. Enable the batch middleware before “UseRouting()” middleware.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseODataBatching(); // call before "UseRouting"
    app.UseRouting();
    // ...
}

Now, the OData service can handle $batch request.

Dependency Injection for OData services

There’s another overload of “AddModel” to enable dependency injection of OData services as:

ODataOptions AddModel(string prefix, IEdmModel model, Action<IContainerBuilder> configureAction);

For example, customer can inject his own deserializer provider using the following codes:

services.AddOData(opt => opt.AddModel("odata", model, builder => builder.AddService<ODataDeserializerProvider>(Microsoft.OData.ServiceLifetime.Singleton, sp => new MyDeserializerProvider(sp)));

Query options

Query options (such as $fitler, $count, etc.) are disabled by default for security. However, it’s easy to enable it by calling method on the ODataOptions.

For example, we can call query options related methods after model configuration as:

services.AddOData(opt => opt.AddModel("odata", GetEdmModel()).Filter().Select().Expand());

The above codes enable $filter, $select and $expand. Then, we can send request GET http://localhost:5000/odata/Books?$filter=Price le 50&$expand=Press($select=Name)&$select=Location($select=City)

We can get:

{
  "@odata.context": "http://localhost:5000/odata/$metadata#Books(Location/City,Press(Name))",
  "value": [
  {
    "Location": {
      "City": "Bellevue"
      },
    "Press": {
      "Name": "O'Reilly"
      }
    }
  ]
}

Routing

OData routing takes the responsibility to match the incoming HTTP requests and dispatch those requests to the app’s executable endpoints, especially the action in the OData controller.

This section, a test middleware is used to introduce some basic ideas of the routing in ASP.NET Core OData 8.0. For more detail routing, please looking forward to the next post called routing in ASP.NET Core OData 8.0.

So, Let’s create a simple middleware between “UseRouting()” and “UseEndpoints()” as:

public class Startup
{   
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddOData(opt => opt.AddModel("odata", GetEdmModel())
            .AddModel("v{version}", GetEdmModel())
            .Filter().Select().Expand());
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        // a test middleware
        app.Use(next => context =>
        {
            var endpoint = context.GetEndpoint();
            if (endpoint == null)
            {
             return next(context);
            }

            IEnumerable templates;
            IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata<IODataRoutingMetadata>();
            if (metadata != null)
            {
                templates = metadata.Template.GetTemplates();
            }

            return next(context); // put a breaking point here
        });

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapODataRoute("odata1", "v1", GetEdmModel());
        });
    }
}

The middleware simply retrieves the selected endpoint from the HttpContext, then retrieves the OData routing metadata defined on the endpoint.

Let’s run it as debug, send request as GEThttp://localhost:5000/v3/Books(1), then stop at a breaking point in this middleware.

Below is the debug information on the Endpoint.

We can find that there’s an endpoint (BookStore.Controllers.BooksController.Get(…) action) selected. The “RoutePattern” property of this endpoint has “v{version}/Books({key})” template associated. This is created by built-in convention routing.

Besides, the endpoint has “IODataRoutingMetadata” associated. Below is the debug information on OData routing metadata.

We can find that the “Prefix” is “v{version}”. Importantly, it has an OData path template with 2 template segments associated. One is entity set segment template, the other is key segment template. The OData template is used to generate the route pattern and the real OData path for a certain request. Bot built-in convention routing, attribute routing and path/segment template will be discussed in next post.

Summary

Thanks for reading and trying. This post is a simple introduction about the ASP.NET Core OData 8.0. We encourage you to download it and start building amazing OData service running on ASP.NET Core 5.

You can refer to here for the sample project created in this blog. Any questions or concerns, feel free email to saxu@microsoft.com

7 comments

Leave a comment