AutoMapper / AutoMapper.Extensions.OData

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

Problem with open type #211

Open Robelind opened 3 months ago

Robelind commented 3 months ago

I'm trying to use an open type in my EDM, but I run into a problem when applying queries.

    public class TestData
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Value { get; set; }
    }

    public class Test
    {
        public int Id { get; set; }
        public ICollection<TestData> Data { get; set; }
    }

    public class TestDTO
    {
        [Key]
        public int Id { get; set; }
        public Dictionary<string, object> Properties { get; set; } = new();
    }

    [ApiController]
    [Route("api/[controller]")]
    public class TestsController : ControllerBase
    {
        public async Task<IActionResult> Get(ODataQueryOptions<TestDTO> options)
        {
            IMapper mapper = new Mapper(new MapperConfiguration(x =>
            {
                x.CreateMap<Test, TestDTO>().ForMember(dest => dest.Properties,
                    cfg => cfg.MapFrom(src => src.Data.Select(d => new KeyValuePair<string, object>(d.Name, d.Value))));
            }));
            IEnumerable<Test> entities =
            [
                new Test { Id = 1, Data = [new TestData { Name = "Value", Value = 10 }] },
                new Test { Id = 2, Data = [new TestData { Name = "Value", Value = 100 }] }
            ];

            return(Ok(await entities.AsQueryable().GetQueryAsync(mapper, options)));
        }
    }

If I do e.g. http://localhost:52769/api/Tests?$orderby=Value it results in the following exception:

System.InvalidCastException: Unable to cast object of type 'Microsoft.OData.UriParser.SingleValueOpenPropertyAccessNode' to type 'Microsoft.OData.UriParser.SingleValuePropertyAccessNode'.
   at AutoMapper.AspNet.OData.LinqExtensions.<GetOrderByCall>g__GetMethodCall|10_0(<>c__DisplayClass10_0&)
   at AutoMapper.AspNet.OData.LinqExtensions.GetOrderByCall(Expression expression, OrderByClause orderByClause, ODataQueryContext context)
   at AutoMapper.AspNet.OData.LinqExtensions.GetQueryableMethod(Expression expression, ODataQueryContext context, OrderByClause orderByClause, Type type, Nullable`1 skip, Nullable`1 top)
   at AutoMapper.AspNet.OData.LinqExtensions.GetOrderByMethod[T](Expression expression, ODataQueryOptions`1 options, ODataSettings oDataSettings)
   at AutoMapper.AspNet.OData.LinqExtensions.GetQueryableExpression[T](ODataQueryOptions`1 options, ODataSettings oDataSettings)
   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 CompactStore.API.Controllers.TestsController.Get(ODataQueryOptions`1 options) in C:\Users\wlsrlm\source\repos\Compact Store NextGen\CompactStore\API\Controllers\ItemsController - Copy.cs:line 120
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(ActionContext actionContext, 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.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   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)
BlaiseD commented 3 months ago

"$orderby=Value" - Value is not a member of TestDTO is it?

Robelind commented 3 months ago

Yes it is. That's the point of open OData types, that you can create dynamic properties. Open types

BlaiseD commented 3 months ago

Maybe I'm misunderstanding.

Try running the query without the orderyby then post the result showing the 'Value field. Do the same using OData without this library.

If OData shows a Value a field and GetQueryAsync does not then you're welcome to submit a PR.

Robelind commented 3 months ago

http://localhost:52769/api/Tests:

{
    "@odata.context": "http://localhost:52769/api/$metadata#Tests",
    "value": [
        {
            "Id": 1,
            "Value": 10
        },
        {
            "Id": 2,
            "Value": 100
        }
    ]
}

The result is the same with or without Automapper. The problem arises when applying sorting.

BlaiseD commented 3 months ago

Ok - that node type is not being handled. I think the code you're looking for is here if you're interested in a PR.

antonGritsenko commented 1 week ago

Actually, open types are not supported at all by AutoMapper OData, this is not the only place. You will have also problem during filtering, grouping etc. The worst case if EFCore is behind of the LINQ query with https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations, then it will not work properly at all.