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 and the ASP.NET Core platforms. Provides the fundamental infrastructure, production-ready startup templates, application modules, UI themes, tooling, guides and documentation.
https://abp.io
GNU Lesser General Public License v3.0
12.42k stars 3.34k forks source link

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

Open antonGritsenko opened 1 month ago

antonGritsenko commented 1 month 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 1 month 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");