AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

$apply is not showing meta data info when I use IActionResult

Open komdil opened this issue 3 years ago • 24 comments

I have Get action in controller:

        public IActionResult Get(ODataQueryOptions<TEntity> queryOptions, CancellationToken cancellationToken)
        {
            var query = MyDBContext.Set<Student>().ToList();
            return Ok(query);
        }

When I send this query: Student?$apply=aggregate($count as OrderCount) It is returning value without meta data.

Actual:

[
    {
        "OrderCount": 3
    }
]

Expected:

{
    "@odata.context": "https://localhost:44383/$metadata#Student(OrderCount)",
    "value": [
        {
            "@odata.id": null,
            "OrderCount": 3
        }
    ]
}

It is working fine when Get looks like:

        public IEnumerable<TEntity> Get(ODataQueryOptions queryOptions, CancellationToken cancellationToken)
        {
            return MyDBContext.Set<TEntity>().ToList();
        }

It is causing problems when entity is Open Type. In open type it looks like:

{
    "$type": "System.Linq.EnumerableQuery`1[[Microsoft.AspNet.OData.Query.Expressions.NoGroupByAggregationWrapper, Microsoft.AspNetCore.OData]], System.Linq.Queryable",
    "$values": [
        {
            "$id": "1",
            "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib",
            "OrderCount": 3
        }
    ]
}

My project is in .NET 5

komdil avatar May 24 '21 12:05 komdil

And in this first case, " $count=true " has no effect.

ekomut avatar Jun 10 '21 14:06 ekomut

In use:

  • .NET 5
  • Microsoft.AspNetCore.OData 8.0.0-rc3
  • Microsoft.EntityFrameworkCore.SqlServer 5.0.4
  • Microsoft.EntityFrameworkCore.Tools 5.0.4
  • Microsoft.EntityFrameworkCore.Proxies 5.0.4 (currently required for Lazy Loading when using $expand)

I am pretty happy to be able to use §apply and groupby now, like for example:

  • {{baseUrl}}/odata/Article?$apply=groupby((Brand/Id))&$top=10&$skip=30&$orderby=Brand/Id&$count=true
  • {{baseUrl}}/odata/Article?$orderby=Brand/Name&$apply=filter(contains(Brand/Name,'de'))/groupby((Brand/Name))&$count=true

Filter, groupBy, orderBy and paging with top and skip actually work.

But the result of those queries do not conform to the usual OData-Results and therefor do not contain the count and nextLink for example. One of those Properties is required to be able to page through the results without ending up on pages that return no results. The count is also very important to indicate the number of datasets available for a certain query.

A usual OData-Result is returned by queries without $apply:

Request-Url: "{{baseUrl}}/odata/Article?$select=Id&$expand=Brand($select=Name)&$top=3&$count=true"
Response:
{
    "@odata.context": "https://{{baseUrl}}/odata/$metadata#Article(Id,Brand(Name))",
    "@odata.count": 13023,
    "value": [
        {
            "Id": 1,
            "Brand": {
                "Name": "Example1"
            }
        },
        {
            "Id": 2,
            "Brand": {
                "Name": "Example2"
            }
        },
        {
            "Id": 3,
            "Brand": {
                "Name": "Example3"
            }
        }
    ]
}

When using $apply the wrapping OData-Result-Object is missing which deliveres @odata.count:

Request-Url: "{{baseUrl}}/odata/Article?$apply=groupby((Brand/Id))&$top=3&$skip=30&$orderby=Brand/Id&$count=true"
Response:
[
    {
        "Brand": {
            "Id": 1302
        }
    },
    {
        "Brand": {
            "Id": 1303
        }
    },
    {
        "Brand": {
            "Id": 1304
        }
    }
]

I did not notice until now, but I am also missing @odata.nextLink which has already been returned on previous versions for usual OData-Queries without $apply.

Our actions look like this:

[EnableQuery]
public IActionResult Get()
{
	var dbContext = HttpContext.RequestServices.GetService<TDbContext>();
	var dbSet = dbContext.Set<TEntity>();

	return Ok(dbSet);
}

JanKotschenreuther avatar Jun 11 '21 20:06 JanKotschenreuther

i'm going to follow this, i have the same problem (missing meta data info) when using IQueryable as controller action result.

public IQueryable<Student> Get(ODataQueryOptions<TEntity> queryOptions, CancellationToken cancellationToken)
      {
          var query = MyDBContext.Set<Student>().AsQueryable()
          return Ok(query);
      }

used packages: NET Core 5 Microsoft.AspNetCore.OData: 8.0.1 Microsoft.AspNetCore.Mvc.NewtonsoftJson: 5.0.7

vonckm-kadaster avatar Jul 14 '21 06:07 vonckm-kadaster

Is there any updates here?

komdil avatar Jan 10 '22 08:01 komdil

Any progress?

komdil avatar Feb 04 '22 11:02 komdil

@komdil and @vonckm-kadaster , a "payload without metadata" is a normal AspNetCore MVC payload, which means your endpoints are not being detected as actual OData endpoints.

Can you try changing the signatures and/or using attribute routing?

For example, I think the extra CancellationToken parameter might cause issues with the default conventions and you may have to go with an explicit route using [HttpGet] with a route template.

Regardless, please use the route debug middleware to make sure your routes are actually OData routes.

julealgon avatar Feb 04 '22 15:02 julealgon

I had the same issue and after debuging I found that the ODataOutputFormatter is returning false in the method CanWriteResult because it seems to be unable to determine the type on line 127-131

The ODataOutputFormatter.CanWriteResult returns false because the context.ObjectType is not set and the context.Object.GetType() returns an IEnumerable<GroupByWrapper> or descending class of GroupByWrapper for which no proper PayLoadKind can be determined. This occurred when an ObjectResult is used as controller action result. When IQueryable<> is used as controller action result the context.ObjectType is set in the ODataOutputFormatter.CanWriteResult and a proper PayLoadKind is determined.

As solution I created a custom EnableQueryAttribute to use instead and override the method OnActionExecuted(ActionExecutedContext actionExecutedContext). This sets the context.ObjectType when the method ODataOutputFormatter.CanWriteResult is entered and a proper PayLoadKind can be determined and result in OData-result

        public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext is null)
            {
                throw new ArgumentNullException(nameof(actionExecutedContext));
            }

            if (actionExecutedContext.HttpContext.Response != null &&
                IsSuccessStatusCode(actionExecutedContext.HttpContext.Response.StatusCode) &&
                actionExecutedContext.Result is ObjectResult content &&
                content.Value != null &&
                content.DeclaredType == null)
            {
                // To help the `ODataOutputFormatter` to determine the correct output class
                // the `content.DeclaredType` needs to be set before appling the `$apply`-option
                // so that a valid `OData`-result is produced
                // https://github.com/OData/AspNetCoreOData/blob/4de92f52a346606a447ec4df96c5f3cd05642f50/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs#L127-L131
                content.DeclaredType = content.Value.GetType();
            }

            base.OnActionExecuted(actionExecutedContext);
        }

        private static bool IsSuccessStatusCode(int statusCode)
        {
            return statusCode >= 200 && statusCode < 300;
        }

cympatic avatar Dec 31 '22 15:12 cympatic

@cympatic could you share the controller code where you had to use this custom attribute on?

julealgon avatar Dec 31 '22 18:12 julealgon

Not directly. However, it's easily reproducible by using the E2E EntitySetAggregation tests in this repository.

The EntitySetAggregationTests.AggregationOnEntitySetWorks(method: "average", expected: 100) succeed normal where the Get -method in the EntitySetAggregationController is

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

and the result will be: {"value":[{"Orders":[{"TotalPrice":100.0}]}]}

But changing the Get-method in the EntitySetAggregationController to

        [EnableQuery]
        public IActionResult Get()
        {
            return Ok(_context.Customers);
        }

Then the result will be [{"Orders":[{"TotalPrice":100}]}]

Creating a custom EnableQueryAttribute like

    public class MyEnableQueryAttribute : EnableQueryAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext is null)
            {
                throw new ArgumentNullException(nameof(actionExecutedContext));
            }

            if (actionExecutedContext.HttpContext.Response != null &&
                IsSuccessStatusCode(actionExecutedContext.HttpContext.Response.StatusCode) &&
                actionExecutedContext.Result is ObjectResult content &&
                content.Value != null &&
                content.DeclaredType == null)
            {
                // To help the `ODataOutputFormatter` to determine the correct output class
                // the `content.DeclaredType` needs to be set before appling the `$apply`-option
                // so that a valid `OData`-result is produced
                // https://github.com/OData/AspNetCoreOData/blob/4de92f52a346606a447ec4df96c5f3cd05642f50/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs#L127-L131
                content.DeclaredType = content.Value.GetType();
            }

            base.OnActionExecuted(actionExecutedContext);
        }

        private static bool IsSuccessStatusCode(int statusCode)
        {
            return statusCode >= 200 && statusCode < 300;
        }
    }

and updating the Get-method to

        [MyEnableQuery]
        public IActionResult Get()
        {
            return Ok(_context.Customers);
        }

will result in {"value":[{"Orders":[{"TotalPrice":100.0}]}]}

And the specific test will succeed again

A commit pushed to the fork I've: example issue 181.

cympatic avatar Dec 31 '22 19:12 cympatic

Not directly. However, it's easily reproducible by using the E2E EntitySetAggregation tests in this repository.

The EntitySetAggregationTests.AggregationOnEntitySetWorks(method: "average", expected: 100) succeed normal ...

Maybe I'm missing something obvious, but that test doesn't work at all for me:

Message:  System.InvalidOperationException : The LINQ expression '(GroupByShaperExpression: KeySelector: new NoGroupByWrapper(), ElementSelector:(EntityShaperExpression: EntityType: Customer ValueBufferExpression: (ProjectionBindingExpression: EmptyProjectionMember) IsNullable: False ) ) .SelectMany($it => $it.Orders)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Stack Trace:  RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) MethodCallExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) MethodCallExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) MethodCallExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression) RelationalProjectionBindingExpressionVisitor.Visit(Expression expression) RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment) ExpressionVisitor.VisitMemberBinding(MemberBinding node) RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression) MemberInitExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) RelationalProjectionBindingExpressionVisitor.Visit(Expression expression) RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment) ExpressionVisitor.VisitMemberBinding(MemberBinding node) RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression) MemberInitExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) RelationalProjectionBindingExpressionVisitor.Visit(Expression expression) RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression) RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector) QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) MethodCallExpression.Accept(ExpressionVisitor visitor) ExpressionVisitor.Visit(Expression node) QueryCompilationContext.CreateQueryExecutor[TResult](Expression query) Database.CompileQuery[TResult](Expression query, Boolean async) QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async) <>c__DisplayClass12_01.<ExecuteAsync>b__0() CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler) CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func1 compiler) QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken) EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken) EntityQueryable1.GetAsyncEnumerator(CancellationToken cancellationToken) AsyncEnumerableReader.ReadInternal[T](Object value) ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, Object asyncEnumerable, Func2 reader) ResourceInvoker.<InvokeResultAsync>g__Logged|21_0(ResourceInvoker invoker, IActionResult result) ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) ResourceInvoker.Rethrow(ResultExecutedContextSealed context) ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) ResourceInvoker.InvokeResultFilters() --- End of stack trace from previous location where exception was thrown --- ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) <<SendAsync>g__RunRequestAsync|0>d.MoveNext() --- End of stack trace from previous location where exception was thrown --- ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) CookieContainerHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) HttpClient.FinishSendAsyncBuffered(Task1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts) EntitySetAggregationTests.AggregationOnEntitySetWorks(String method, Int32 expected) line 65 --- End of stack trace from previous location where exception was thrown ---

julealgon avatar Dec 31 '22 20:12 julealgon

That's strange. I just forked this repository, cloned it to my machine, removed the Skip in the Theory-attribute on that test, and ran the test as-is. The database EntitySetAggregationContext is created based on the connectionstring Server=(localdb)\mssqllocaldb;Database=EntitySetAggregationContext;Trusted_Connection=True; as expected and the test is executed successfully.

cympatic avatar Dec 31 '22 21:12 cympatic

OData.Issue.181.zip I've created and attached a small sample project based on the EntitySetAggregationTests. Hopefully this helps

cympatic avatar Jan 01 '23 12:01 cympatic

Hopefully this helps

It does.

Took me a while to realize how to make this work (for some weird reason...), but I can see now how it's supposed to be handled.

Change the method as per the following:

    [EnableQuery]
    [HttpGet("AsActionResult")]
    public ActionResult<IQueryable<Customer>> AsActionResult()
    {
        return _context.Customers;
    }

The conversion from T to ActionResult<T> actually generates an ObjectResult with populated DeclaredType, which makes everything work as expected.

Using IActionResult is mostly considered a bad practice these days when returning typed data from controllers (should be reserved to void-returning methods).

In this scenario, I'd not recommend any changes be made to OData/EnableQueryAttribute at all.

julealgon avatar Jan 02 '23 13:01 julealgon

The change

    [EnableQuery]
    [HttpGet("AsActionResult")]
    public ActionResult<IQueryable<Customer>> AsActionResult()
    {
        return _context.Customers;
    }

result still in a JSON result and not in an OData result when $apply is used. When this method results in a proper OData result the tests for this method will fail.

The conversion from T to ActionResult<T> actually generates an ObjectResult with populated DeclaredType, which makes everything work as expected. This is not true when using $apply. This only work when IQueryable<Customer> is used.

Using IActionResult works for queries without $apply and isn't the issue. It's when $apply is used as mentioned in my earlier post

The ODataOutputFormatter.CanWriteResult returns false because the context.ObjectType is not set and the context.Object.GetType() returns an IEnumerable<GroupByWrapper> or descending class of GroupByWrapper for which no proper PayLoadKind can be determined. This occurred when an ObjectResult is used as controller action result. When IQueryable<> is used as controller action result the context.ObjectType is set in the ODataOutputFormatter.CanWriteResult and a proper PayLoadKind is determined.

cympatic avatar Jan 02 '23 19:01 cympatic

The change

    [EnableQuery]
    [HttpGet("AsActionResult")]
    public ActionResult<IQueryable<Customer>> AsActionResult()
    {
        return _context.Customers;
    }

result still in a JSON result and not in an OData result when $apply is used.

It definitely works for me:

image

When this method results in a proper OData result the tests for this method will fail.

The test is failing for me with that change.

EDIT: @cympatic did you perhaps forget to remove the Ok(...)? That is a very subtle detail but it actually makes a difference.

julealgon avatar Jan 02 '23 20:01 julealgon

@cympatic did you perhaps forget to remove the Ok(...)? That is a very subtle detail but it actually makes a difference.

Thanks! That was indeed the problem, I missed that

cympatic avatar Jan 02 '23 20:01 cympatic

@julealgon is it fair to say that this issue is solved? Or at least explained well enough?

cympatic avatar Jan 03 '23 10:01 cympatic

@julealgon is it fair to say that this issue is solved? Or at least explained well enough?

As I mentioned above, from my perspective this appears to be working properly considering how using ActionResult<T> for modern projects is the standard way of providing the return type at compile time.

I'm not part of the OData team however, so I can't close this myself.

Either @komdil can validate and close, or we can wait for someone on the team to take a look.

julealgon avatar Jan 03 '23 12:01 julealgon

Prior to closing I would like to encourage updating samples / documentation at minimum to reflect the requirement of using ActionResult<T> over IActionResult.

jimbromley-avanade avatar Jan 11 '23 15:01 jimbromley-avanade

@cympatic Thanks for bringing this up and providing a workaround!

In my case I had to apply the ODataQueryOptions on my own. The documentation shows this example:

public IQueryable<Customer> Get(ODataQueryOptions<Customer> options)
{
    IQueryable results = options.ApplyTo(Customers.AsQueryable());

    return results as IQueryable<Customer>;
}

But this code doesn't work if select or apply is used because after applying the query isn't of type IQueryable<Customer>. So the code needs to be changed to something like:

public IQueryable Get(ODataQueryOptions<Customer> options)
{
    IQueryable results = options.ApplyTo(Customers.AsQueryable());

    return results;
}

This in turn brings up the problem from this reported issue. I ended up with a simple ActionFilterAttribute that I can attach to these special cases:

public class ODataTypeAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
    {
        if (actionExecutedContext is null)
        {
            throw new ArgumentNullException(nameof(actionExecutedContext));
        }

        if (actionExecutedContext.HttpContext.Response != null &&
            IsSuccessStatusCode(actionExecutedContext.HttpContext.Response.StatusCode) &&
            actionExecutedContext.Result is ObjectResult content &&
            content.Value != null &&
            (content.DeclaredType == typeof(ActionResult) || content.DeclaredType == typeof(IQueryable) 
            || content.DeclaredType == typeof(Task<ActionResult>) || content.DeclaredType == typeof(Task<IQueryable>))
            )
        {
            // To help the `ODataOutputFormatter` to determine the correct output class
            // the `content.DeclaredType` needs to be set so that a valid `OData`-result is produced
            // https://github.com/OData/AspNetCoreOData/blob/4de92f52a346606a447ec4df96c5f3cd05642f50/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs#L127-L131

            var returnType = content.Value.GetType();
            if (returnType.IsConstructedGenericType)
            {
                returnType = returnType.GenericTypeArguments[0];
            }

            content.DeclaredType = returnType;
        }

        base.OnActionExecuted(actionExecutedContext);
    }

    private static bool IsSuccessStatusCode(int statusCode)
    {
        return statusCode >= 200 && statusCode < 300;
    }
}

@xuzhg Documentation needs to be adjusted and is there a real solution to that problem?

audacity76 avatar Mar 26 '24 10:03 audacity76

summary to any newcomer

Date: 2024-04-12 .NET: 8.0.4 OData: 8.2.5

Official document example: image

My Controller Action -- return OkObjectResult

public ActionResult<IQueryable<Person>> GetPeople()
{
  return Ok(db.People); 
}

Simple Query: http://localhost:5237/api/people

{
  "@odata.context": "http://localhost:5237/api/$metadata#people",
  "value": [
    {
      "id": 1,
      "name": "Derrick",
      "startDate": "2023-01-01",
      "timeOfDay": "16:50:00.0000000"
    },
    {
      "id": 2,
      "name": "Derrick",
      "startDate": "2023-01-01",
      "timeOfDay": "16:50:00.0000000"
    }
  ]
}

perfect! Query with $apply: http://localhost:5237/api/people?$apply=groupby((name),aggregate($count as count))&$count=true

[
  {
    "name": "Derrick",
    "count": 2
  }
]

missing @odata.context and @odata.count

My Controller Action -- return IQueryable

public ActionResult<IQueryable<Person>> GetPeople()
{
  return db.People;
}

Query with $apply: http://localhost:5237/api/people?$apply=groupby((name),aggregate($count as count))&$count=true

{
  "@odata.context": "http://localhost:5237/api/$metadata#people(name,count)",
  "@odata.count": 1,
  "value": [
    {
      "@odata.id": null,
      "name": "Derrick",
      "count": 2
    }
  ]
}

perfect!

The conclusion is: The example on the official website only applies to cases without $apply. In cases where $apply is used, OkObjectResult cannot be returned; IQueryable must be returned directly. It is recommended that everyone always return IQueryable.

keatkeat87 avatar Apr 12 '24 10:04 keatkeat87

@keatkeat87 , i think there's a typo in your comment,

My Controller Action -- return IQueryable

public ActionResult<IQueryable<Person>> GetPeople()
{
  return db.People;
}

Do you mean , your controller is :

public IQueryable<Person>GetPeople()
{
  return db.People;
}

I'am having serialization issue with Odata Client due to this problem of missing meta data info. Isn't considered a bad habbit to return IQueryable to the client ?

araies avatar Jun 05 '24 10:06 araies

@keatkeat87 , i think there's a typo in your comment,

My Controller Action -- return IQueryable

public ActionResult<IQueryable<Person>> GetPeople()
{
  return db.People;
}

Do you mean , your controller is :

public IQueryable<Person>GetPeople()
{
  return db.People;
}

@araies that's not a typo, that's how it is supposed to look/work. ActionResult<T> has a conversion from T and returning the IQueryable<T> there works and properly sets the type on the underlying result object.

I'am having serialization issue with Odata Client due to this problem of missing meta data info. Isn't considered a bad habbit to return IQueryable to the client ?

There is no such bad habit. Where did you hear that?

julealgon avatar Jun 05 '24 12:06 julealgon

ActionResult<IQueryable<Person>> @julealgon , Indeed, its works with EF dbset, in my sample code i'm working on an IQueryable<T> not a dbSet and my code dosen't compile.

I've tried the solution of overriding the OnActionExecuted Method of EnableQueryAttribute : As mentioned by @cympatic

This resolves the issue of using Aggregation and grouping, and my Odata Client is serializing perfectly the Odata service Response even when my controller is returning a ActionResult<IEnumerable<T>>

For the returning IQueryable to the client, in my case , a query should be validated and maybe some additional conditions and clauses will be added to the query before executing it. I fear that exposing IQueryable to the client will give it more control.

araies avatar Jun 05 '24 13:06 araies