AutoMapper/AutoMapper.Extensions.OData

Problem with open type

Robelind opened this issue · 6 comments

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)

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

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

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.

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.

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.

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.