abpframework / abp

Open-source web application framework for ASP.NET Core! Offers an opinionated architecture to build enterprise software solutions with best practices on top of the .NET. Provides the fundamental infrastructure, cross-cutting-concern implementations, startup templates, application modules, UI themes, tooling and documentation.
https://abp.io
GNU Lesser General Public License v3.0
12.94k stars 3.45k forks source link

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

Open antonGritsenko opened 6 months ago

antonGritsenko commented 6 months ago

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()\r\n .Where(i => ef_filter__p_0 || !(EF.Property(i, \"IsDeleted\")) && ef_filterp_1 || (Guid?)EF.Property(i, \"TenantId\") == __ef_filterCurrentTenantId_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.gCheckTranslated|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.<>cDisplayClass9_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.g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

antonGritsenko commented 6 months ago

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 commented 4 months ago

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.