anthino12
anthino12

Reputation: 958

OData doesn't return context and count after upgrading to .NET 6

I used .NET 3.1 until I decided to upgrade my app to .NET 6. I did it successfully but some of my modules broke, one of them is OData. I used OData and I got @odata.context, @odata.count and value returned back in 3.1. Now in .NET 6, I get only the value which means that @odata.context and @odata.count aren't present in the return object. In order to make it run, I added this line in my code services.AddControllers().AddOData(options => options.AddRouteComponents("v1", GetEdmModel()).Select().Filter().OrderBy()); in my Startup.cs where

private static IEdmModel GetEdmModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Project>("Project");
    return builder.GetEdmModel();
}

My endpoint is as

[HttpGet]
[EnableQuery(PageSize = 20)]
public async Task<ActionResult> GetAsync()
{
    var projects = await _projectRepository.GetAllAsync();
    return Ok(projects);
}

Do you know what can I change and how should I change it in order to get the context and count from OData in .NET 6? I use the usual OData library with 8.0.0 version

EDIT to add more info about @Tiny Wang's answer: This seems to be working and thank you very much for it! However, I stumbled upon another problem. I tried your example and the working version was on https://localhost:44327/odata/project$count=true&$skip=0&$orderby=CreateDate%20desc. My api prefix is api/[ControllerName] and I changed

options => options.EnableQueryFeatures().AddRouteComponents("odata", GetEdmModel()).Select().Filter().OrderBy()

to

options => options.EnableQueryFeatures().AddRouteComponents("api", GetEdmModel()).Select().Filter().OrderBy()

but when I access the endpoint, I get the following error:

"The request matched multiple endpoints. Matches: MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api) MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api)"

even tho GetAsync() is defined only once. Do you know how can I fix this and what causes it?

Upvotes: 1

Views: 7270

Answers (2)

Tiny Wang
Tiny Wang

Reputation: 15991

enter image description here

==================================

Per my test(created a .net 6 web api project and install Microsoft.AspNetCore.OData 8.0.10), I need to add ?$count=true behind my url then it can appear in my response.

My program.cs

builder.Services.AddControllers().AddOData(options => options.EnableQueryFeatures().AddRouteComponents("odata", GetEdmModel()).Select().Filter().OrderBy());

IEdmModel GetEdmModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<WeatherForecast>("Hello");
    return builder.GetEdmModel();
}

My test controller:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace WebApi.Controllers
{
    [Route("odata/[Controller]")]
    public class HelloController : ODataController
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [EnableQuery]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Id = index,
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

enter image description here enter image description here

Upvotes: 1

Panagiotis Kanavos
Panagiotis Kanavos

Reputation: 131512

The change was in the Microsoft.AspNetCore.OData package, not .NET 6. You can use that package in .NET 5 if you want.

First of all, you always had to specify $count=true. That's an expensive operation. In a paging query it meant you had to execute two queries, one to receive a single page of data and another to count all possible results.

To do that you have to enable counting, just like any other OData operation, in the AddOData() call that adds the OData middleware.

Finally, for this to have any real effect, the controller action should return an IQueryable that will be used to construct the final LINQ and by extension SQL query. Otherwise your code will load everything in memory. You'd load a 100K row table in memory only to return 10 rows

OData failed to gain traction because it allowed clients to execute inefficient and unoptimized queries. Service developers had no idea what to optimize because client developers were free to execute anything. In later versions of the protocol, all query capabilites are off by default and have to be explicitly enabled. Server developers now can restrict the maximum size, whether expensive sort operations are allowed etc. They can prevent client code from filtering or sorting by unindexed fields for example.

In my own application I add the OData middleware with :

var edmModel=ODataModels.GetEdmModel();
services.AddControllersWithViews()
        .AddOData(opt => opt.Select()
                            .OrderBy()
                            .Filter()
                            .Count()
                            .Expand()
                            .SetMaxTop(250)
                            .AddRouteComponents("odata", edmModel)
            );

This enables Count and sets the maximum result size to a fairly large 250 - I have some grids users tend to scroll through.

To use the OData endpoints, a Controller that inherits from ODataController is needed. Query methods should return an IQueryable. If that IQueryable comes from a DbContet, the OData query will be used to construct a LINQ query and by extension the final SQL query. This will ensure that only the necessary data will be loaded.

    [EnableQuery]
    public IQueryable<Customers> Get()
    {
        return _db.Customers.AsNoTracking();
    }

An OData controller that works on top of EF Core could look like this :

public class CustomersController:ODataController
{
    private readonly ILogger<CustomersController> _logger;
    private readonly SalesContext _db;


    public CustomersController(SalesContext db, ILogger<CustomersController> logger)
    {
        _logger = logger;
        _db = db;
    }

    [EnableQuery]
    public IQueryable<Customers> Get()
    {
        return _db.Customers.AsNoTracking();
    }
    
    [EnableQuery]
    [HttpGet]
    public IActionResult Get(long key)
    {
        var cust = _db.Customers
                      .AsNoTracking()
                      .FirstOrDefault(t => t.ID == key);
        if (cust == null)
        {
            return NotFound($"Not found: Customer ID = {key}");
        }

        return Ok(cust);
    }

...

The Get(key) action is necessary to allow retrieving items by ID in OData eg using customers(123).

Upvotes: 3

Related Questions