Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting OData Count in ASP.NET Core WebAPI

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:

enter image description here

What needs to be configured to get the response object to be wrapped in an OData context so that the @odata.count property will show?

In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.

With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:

enter image description here

I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.

My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)

like image 410
PoorInRichfield Avatar asked Nov 13 '19 14:11

PoorInRichfield


4 Answers

Just been battling this.

I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.

However, $count does work if I request the same method via /odata/Things.

like image 86
Richard Barraclough Avatar answered Oct 13 '22 05:10

Richard Barraclough


You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.

routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());
like image 26
Enrique Raso Avatar answered Oct 13 '22 05:10

Enrique Raso


I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:

app.UseMvc(routeBuilder =>
        {
            routeBuilder.Expand().Select().Count().OrderBy().Filter();
            routeBuilder.EnableDependencyInjection();
        });

To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.

Here is a simple demo:

1.Controller(comment on Route attribute and ApiController attribute):

//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
    private readonly WSDbContext _context;
    public StudentsController(WSDbContext context)
    {
        _context = context;
    }
    // GET: api/Students
    [HttpGet]
    [EnableQuery()]
    public IEnumerable<Student> Get()
    {
        return _context.Students;
    }
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
    private readonly WSDbContext _context;
    public SchoolsController(WSDbContext context)
    {
        _context = context;
    }
    // GET: api/Schools
    [HttpGet]
    [EnableQuery()]
    public IEnumerable<School> Get()
    {
        return _context.Schools;
    }

2.Startup.cs():

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore(action => action.EnableEndpointRouting = false);
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        var connection = @"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
        services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
        services.AddOData();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseMvc(routeBuilder =>
        {
            routeBuilder.Expand().Select().Count().OrderBy().Filter();
            routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
        });
    }

    private static IEdmModel GetEdmModel()
    {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<Student>("Students");
        builder.EntitySet<Student>("Schools");
        return builder.GetEdmModel();
    }
}
like image 34
Rena Avatar answered Oct 13 '22 05:10

Rena


In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.

I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.

public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
    public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);

        if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
        {
            obj.Value = new ODataResponse
            {
                Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
                Value = qry
            };
        }
    }
    public class ODataResponse
    {
        [JsonPropertyName("@odata.count")]
        public long? Count { get; set; }

        [JsonPropertyName("value")]
        public IQueryable Value { get; set; }
    }
}
like image 2
Jess Avatar answered Oct 13 '22 04:10

Jess