AutoMapper / AutoMapper.Extensions.OData

Creates LINQ expressions from ODataQueryOptions and executes the query.
MIT License
140 stars 38 forks source link

$expand with $filter on custom namespace #149

Closed cheng93 closed 1 year ago

cheng93 commented 1 year ago

The exception at the bottom is found

Sample

https://github.com/cheng93/AutoMapper.OData/tree/odata-namespace https://github.com/cheng93/AutoMapper.OData/blob/odata-namespace/Program.cs#L28 (causes the issue)

http://localhost:5238/odata/categories?$expand=Products($filter=ProductId%20eq%201)

System.ArgumentException: Cannot find CLT type for EDM type FooBar.ProductModel
   at AutoMapper.AspNet.OData.TypeExtensions.GetClrType(EdmTypeStructure edmTypeStructure, IDictionary`2 typesCache)
   at AutoMapper.AspNet.OData.TypeExtensions.GetClrType(IEdmTypeReference edmTypeReference, IDictionary`2 typesCache)
   at AutoMapper.AspNet.OData.FilterHelper.GetClrType(IEdmTypeReference typeReference)
   at AutoMapper.AspNet.OData.FilterHelper.GetSingleValuePropertyAccessFilterPart(SingleValuePropertyAccessNode singleValuePropertyAccesNode)
   at AutoMapper.AspNet.OData.FilterHelper.GetFilterPart(SingleValueNode singleValueNode)
   at AutoMapper.AspNet.OData.FilterHelper.GetBinaryOperatorFilterPart(BinaryOperatorNode binaryOperatorNode)
   at AutoMapper.AspNet.OData.FilterHelper.GetFilterPart(SingleValueNode singleValueNode)
   at AutoMapper.AspNet.OData.LinqExtensions.GetFilterExpression(FilterClause filterClause, Type type)
   at AutoMapper.AspNet.OData.Visitors.FilterAppender.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at AutoMapper.AspNet.OData.Visitors.FilterAppender.AppendFilter(Expression expression, ODataExpansionOptions expansion)
   at AutoMapper.AspNet.OData.Visitors.ChildCollectionFilterUpdater.GetBindingExpression(MemberAssignment binding, ODataExpansionOptions expansion)
   at AutoMapper.AspNet.OData.Visitors.ProjectionVisitor.<>c__DisplayClass3_0.<VisitMemberInit>g__AddBinding|0(List`1 list, MemberAssignment binding)
   at System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func)
   at AutoMapper.AspNet.OData.Visitors.ProjectionVisitor.VisitMemberInit(MemberInitExpression node)
   at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)
   at System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node)
   at System.Linq.Expressions.UnaryExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at System.Dynamic.Utils.ExpressionVisitorUtils.VisitArguments(ExpressionVisitor visitor, IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at AutoMapper.AspNet.OData.Visitors.ChildCollectionFilterUpdater.UpdaterExpansion(Expression expression, List`1 expansions)
   at AutoMapper.AspNet.OData.LinqExtensions.<>c__DisplayClass39_1`1.<UpdateQueryableExpression>b__4(List`1 filterList)
   at System.Collections.Generic.List`1.ForEach(Action`1 action)
   at AutoMapper.AspNet.OData.LinqExtensions.<>c__DisplayClass39_0`1.<UpdateQueryableExpression>g__UpdateProjectionFilterExpression|0(Expression projectionExpression)
   at AutoMapper.AspNet.OData.LinqExtensions.UpdateQueryableExpression[TModel](IQueryable`1 query, List`1 expansions, ODataQueryContext context)
   at AutoMapper.AspNet.OData.QueryableExtensions.GetQueryable[TModel,TData](IQueryable`1 query, IMapper mapper, ODataQueryOptions`1 options, QuerySettings querySettings, Expression`1 filter)
   at AutoMapper.AspNet.OData.QueryableExtensions.GetQueryAsync[TModel,TData](IQueryable`1 query, IMapper mapper, ODataQueryOptions`1 options, QuerySettings querySettings)
   at OData.AutoMapper.ApiController.GetODataCategories(ODataQueryOptions`1 options) in C:\Code\cheng93\OData.AutoMapper\ApiController.cs:line 109
   at lambda_method4(Closure , Object )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
BlaiseD commented 1 year ago

Looks like it is working as designed (it needs to find the object type from the EDM type). Ok to submit a PR to support the custom namespace.

cheng93 commented 1 year ago

@BlaiseD I don't believe it does

https://github.com/cheng93/AutoMapper.OData/blob/odata-namespace/ApiController.cs

With EnableQuery and ProjectTo, using namespace works. With GetQueryAsync, it does not work.

Also I believe that OData namespace != EDM namespace

cheng93 commented 1 year ago

Also is it possible to leave the issue open and maybe tag it as a feature request/enhancement?

Someone can pick it up and implement it.

If it remains closed, then it is unlikely this will happen

BlaiseD commented 1 year ago

@cheng93 - the exception here is thrown because it cannot match the EDM type with a loaded type.

I think to reopen, the issue should be clearer about what underlying logic is missing - other than that "EnableQuery and ProjectTo work" i.e. what's the alternative logic.

Make sense?

cheng93 commented 1 year ago

Ok, I suppose the issue is that having a custom OData namespace should not affect matching an edm type with a CLR type. These should be defined by the automapper mappings

http://localhost:5238/odata/categories?$expand=Products expand by itself works too, so I think that implies it has found the CLR type (ProductModel)

however adding the filter, causes the exception which is wierd, as the CLR type is already known

BlaiseD commented 1 year ago

Almost. In most cases the type is known from the initial TModel type e.g. expand by itself. The filter helper calls GetClrType using the EDM type when it does not know the type.

I think a fix will involve passing a type to every GetXXXFilterPart meaning GetClrType will become obsolete.

Not quite obsolete - it will still be needed for constants - just not for members of the initial TModel,