OData, Net5: How to implement $count when controllers return entities of multiple types

Viewed 438

I am adding OData to an existing large API in .Net5, where the action methods in each controller return different types (such as OrganizationFull, OrganizationList, etc.)

This is working, except that the responses do not contain OData meta data, such as $count. So when a consumer calls /api/OrganizationList?$count=true, it doesn't get the count.

I understand that to get this meta data, I have to add an EDM model, like so:

// Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers()
                .AddOData(opt => opt.Select().Filter().Count().SetMaxTop(10).AddRouteComponents("odata", GetEdmModel()))


        IEdmModel GetEdmModel()
        {
            var odataBuilder = new ODataConventionModelBuilder();

            odataBuilder.EntitySet<OrganizationFull>(nameof(Organization));
            odataBuilder.EntitySet<OrganizationList>(nameof(Organization)); // throws
            return odataBuilder.GetEdmModel();
        }


Here I specified 2 EntitySets, one for each entity type returned from the OrganizationController.

However, this throws an exception "The entity set 'Organization' was already configured with a different EntityType ('OrganizationFull').". And if I remove the EntitySet for OrganizationList, when I hit the List end point, I get a 404.

How can I get my end points to return the $count meta data, while keeping my API (which returns multiple entity types from one controller)?

Alternatively, is there a way to get the count in the action method from the OData infrastructure? In that case I could return it in a response header.

2 Answers

After some digging, I found a way to get the count out of the OData library without giving it an EDM model and without changing the API's JSON responses (that is, without adding OData metadata).

This applies to Microsoft.AspNetCore.OData version 8.0.1.

It turns out that the OnActionExecuted method of the EnableQueryAttribute filter exposed by this library associates the count with the http context. You can override this method, retrieve the count, and then do with it what you want (such as creating a response header):

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

        HttpContext context = actionExecutedContext.HttpContext;

        // TotalCount will be null if 1) Count is not enabled during startup; and 2) if
        // $count=true is not used in the url.

        long? count = context.Request.ODataFeature().TotalCount;
        if (count.HasValue)
        {
            // Here you could do something with count, 
            // such as adding it to the response using a response header.
        }
    }
}

You would then decorate your end points with your new attribute instead of EnableQuery. For example:

[EnableQueryWithCount]
public virtual ActionResult<IQueryable<T>> Get()
{
    ...
}

Try to add .Count() to AddOData method call:

           .AddOData(opt => opt.Select().Filter().Count().SetMaxTop(10).AddRouteComponents("odata", GetEdmModel()))
Related