abp icon indicating copy to clipboard operation
abp copied to clipboard

Fix the LINQ injection to make the real support for AspNet.Core.OData in the ABP

Open antonGritsenko opened this issue 9 months ago • 1 comments

ABP Framework >= 6.0

Some issues mentioned "support" of the OData (like this or this), but the real app doesn't work.

The problem is extended properties and injection made into the LINQ by ABP.

Image code like this (using DBContext just for simplicity):

public class UsersController : ODataController
{
    private readonly MyDbContext _dbContext;
    private readonly IObjectMapper _mapper;
    private readonly ODataConventionModelBuilder _builder;


    public UsersController(MyDbContext dbContext, IObjectMapper mapper)
    {
        _dbContext = dbContext;
        _mapper = mapper;

        _builder = new ODataConventionModelBuilder();
        _builder.EntityType<IdentityUser>()
            .HasName("IdentityUser")
            .HasKey(t => t.Id);
        _builder.EntityType<IdentityRole>()
            .HasName("IdentityRole")
            .HasKey(t => t.Id);

        _builder.EntitySet<IdentityUser>("Users");
        _builder.EntitySet<IdentityUser>("Roles");

    }

    public IQueryable<IdentityUserDto> Get(ODataQueryOptions<IdentityUserDto> query)
    {
        

        var model = _builder.GetEdmModel();

        IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Users");
        ODataPath path = new ODataPath(new EntitySetSegment(entitySet));

        var enttyOpts = new ODataQueryContext(model, typeof(IdentityUser), path);

        var opts = new ODataQueryOptions<IdentityUser>(enttyOpts, query.Request);

        var queryable = (IQueryable<IdentityUser>)opts.ApplyTo(_dbContext.Users.AsQueryable<IdentityUser>());
        var result = queryable.ToList();
        return _mapper.Map<List<IdentityUser> ,List<IdentityUserDto>>(result).AsQueryable();
    }
}

That will work and you even can do a very simple query like $filter=name eq 'John'. But as soon as you want to use any "extended" (from ABP point of view) properties like UserName (I have no idea why it counts it as Extended, to be honest) you will get error like this:

The LINQ expression 'DbSet<IdentityUser>()\r\n .Where(i => __ef_filter__p_0 || !(EF.Property(i, "IsDeleted")) && __ef_filter__p_1 || (Guid?)EF.Property<Guid>(i, "TenantId") == __ef_filter__CurrentTenantId_2)\r\n .Where(i => (string)i.ExtraProperties.ContainsKey("UserName") ? i.ExtraProperties["UserName"] : null == __TypedProperty_0)' could not be translated. Additional information: Translation of method 'System.Collections.Generic.Dictionary<string, object>.ContainsKey' failed. If this method can be mapped to your custom function, see https://go.microsoft.com/fwlink/?linkid=2132413 for more information. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|15_0(ShapedQueryExpression translated, <>c__DisplayClass15_0& ) at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression) at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor) at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node) at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_01.<Execute>b__0() at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func1 compiler) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1.GetEnumerator() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at System.Linq.Enumerable.ToList[TSource](IEnumerable1 source) at CommunityManager.OData.UsersController.Get(ODataQueryOptions`1 query) at lambda_method2483(Closure , Object , Object[] ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync() --- End of stack trace from previous location --- 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.<InvokeNextExceptionFilterAsync>g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

antonGritsenko avatar May 08 '24 17:05 antonGritsenko

Connected with #11566 and #10662

OData should somehow use this instead ExtraProperties:

var query = (await GetQueryableAsync()).Where(u => EF.Property<string>(u, "SocialSecurityNumber") == "123");

antonGritsenko avatar May 12 '24 00:05 antonGritsenko

After a lot of digging, I can definitely say that OData support in ABP is not possible because of Extra Property feature. There is no support for this in AutoMapper and in OData library itself. You can build OData for your own entities, but not for OOB. Issue:

  1. You will get an error if try to sort or filter by extended properties (https://github.com/AutoMapper/AutoMapper.Extensions.OData/issues/211)
  2. There is no way map extended properties to EFCore query (at least with current implementation), as in this bug.

Funny fact that both, EFCore and OData, have support for it, but libraries above just do not implement this.

antonGritsenko avatar Jul 13 '24 09:07 antonGritsenko