Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluent mapper + expression based association fails when using generic IQueryable extension. Works when just accessing in Linq #4509

Open
joonatanu-softwerk opened this issue May 17, 2024 · 1 comment · May be fixed by #4510
Assignees
Labels
area: linq status: has-pr There is active PR for issue

Comments

@joonatanu-softwerk
Copy link

joonatanu-softwerk commented May 17, 2024

Describe your issue

In a situation where FluentMappingBuilder is defining an association using expressions for both properties that are being used to join entities together, the generic extension is failing to traverse that association. When i access the same association later in my own LINQ query, it is successfully working.

Exception message: Association key 'MySpecialFieldForPermissionLinking' not found for type 'IHasPermissions`1[User].
Stack trace: linq2db
   at LinqToDB.Linq.Builder.AssociationHelper.CreateAssociationQueryLambda(ExpressionBuilder builder, AccessorMember onMember, AssociationDescriptor association, Type parentOriginalType, Type parentType, Type objectType, Boolean inline, Boolean enforceDefault, List`1 loadWith, Boolean& isLeft)
   at LinqToDB.Linq.Builder.TableBuilder.TableContext.GetContext(Expression expression, Int32 level, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionContext.GetContext(Expression expression, Int32 level, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.TableBuilder.BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSequence(BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.AllAnyBuilder.BuildMethodCall(ExpressionBuilder builder, MethodCallExpression methodCall, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.MethodCallBuilder.BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSequence(BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.GetSubQuery(IBuildContext context, MethodCallExpression expr)
   at LinqToDB.Linq.Builder.ExpressionBuilder.GetSubQueryContext(IBuildContext context, MethodCallExpression expr)
   at LinqToDB.Linq.Builder.ExpressionBuilder.SubQueryToSql(IBuildContext context, MethodCallExpression expression)
   at LinqToDB.Linq.Builder.ExpressionBuilder.ConvertToSql(IBuildContext context, Expression expression, Boolean unwrap, ColumnDescriptor columnDescriptor, Boolean isPureExpression)
   at LinqToDB.Linq.Builder.ExpressionBuilder.ConvertPredicate(IBuildContext context, Expression expression)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSearchCondition(IBuildContext context, Expression expression, List`1 conditions)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildWhere(IBuildContext parent, IBuildContext sequence, LambdaExpression condition, Boolean checkForSubQuery, Boolean enforceHaving)
   at LinqToDB.Linq.Builder.WhereBuilder.BuildMethodCall(ExpressionBuilder builder, MethodCallExpression methodCall, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.MethodCallBuilder.BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSequence(BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.WhereBuilder.BuildMethodCall(ExpressionBuilder builder, MethodCallExpression methodCall, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.MethodCallBuilder.BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSequence(BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.SelectBuilder.BuildMethodCall(ExpressionBuilder builder, MethodCallExpression methodCall, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.MethodCallBuilder.BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.BuildSequence(BuildInfo buildInfo)
   at LinqToDB.Linq.Builder.ExpressionBuilder.Build[T]()
   at LinqToDB.Linq.Query`1.CreateQuery(ExpressionTreeOptimizationContext optimizationContext, ParametersContext parametersContext, IDataContext dataContext, Expression expr)
   at LinqToDB.Linq.Query`1.GetQuery(IDataContext dataContext, Expression& expr, Boolean& dependsOnParameters)
   at LinqToDB.Linq.ExpressionQuery`1.GetQuery(Expression& expression, Boolean cache, Boolean& dependsOnParameters)
   at LinqToDB.Linq.ExpressionQuery`1.get_SqlText()
   at LinqToDB.Linq.ExpressionQueryImpl`1.ToString()
   at System.IO.TextWriter.WriteLine(Object value)
   at System.IO.TextWriter.SyncTextWriter.WriteLine(Object value)
   at System.Console.WriteLine(Object value)
   at Program.<Main>$(String[] args) in C:\src\Linq2DbPlayground\Linq2DbPlayground\Program.cs:line 21

Steps to reproduce

Here's a simplified example of "soft-linked" permission system.
Permission table has a Guid LinkId { get; set; } which defines a link to any object based on that Guid value. It has no foreign keys.
Any entity, that wants to use permissions system, must provide a linking guid in the form of Guid-provider expression and a List<Permission> Permissions property.

Later, when i access the Permissions property, the association works well.
But when i use extension method on based on IQueryable<IHasPermissions<T>>, i get an error.

I'm sorry for a bit complex example. This is how much i was able to consolidate the scenario to still produce an error.
(In real application, i have [RestrictedEntity] attribute, that adds a query filter that uses similar filtering like OnlyWithPermissions() is here.)

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.Mapping;
using System.Linq.Expressions;
using System.Reflection;

using var dbCtx = new MyDbCtx(new DataOptions(new ConnectionOptions()
{
    ConnectionString = "Data Source=:memory:;Version=3;New=True;",
    ProviderName = ProviderName.SQLite
}));

dbCtx.CreateTable<User>();
dbCtx.CreateTable<Permission>();

var query = dbCtx.GetTable<User>()
    .OnlyWithPermissions() // If you comment this out, it works
    .Where(x => x.Name == "MyNameSearch!")
    .Select(x => new { x.Id, x.Name, x.Permissions.Count }); // <- Here i can properly get the count!

Console.WriteLine(query);

var result = query.ToList();

Console.ReadLine();


public class MyDbCtx : DataConnection
{
    public MyDbCtx(DataOptions options) : base(options)
    {
        var builder = new FluentMappingBuilder();

        var baseEntityType = typeof(IBaseEntity);
        var entityTypes = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(x => x.GetTypes())
            .Where(x =>
                !x.IsAbstract
                && x.IsClass
                && x.IsPublic
                && baseEntityType.IsAssignableFrom(x)
            );

        var hasPermissionProcessorMethod = typeof(MyDbCtx).GetMethod(nameof(HasPermissionProcessor), BindingFlags.Static | BindingFlags.NonPublic)!;
        foreach (var entityType in entityTypes)
        {
            var concreteIHasPermission = typeof(IHasPermissions<>).MakeGenericType(entityType);
            if (entityType.IsAssignableTo(concreteIHasPermission))
            {
                hasPermissionProcessorMethod.MakeGenericMethod(entityType).Invoke(null, [builder]);
            }
        }

        builder.Build();

        AddMappingSchema(builder.MappingSchema);
    }

    private static void HasPermissionProcessor<T>(FluentMappingBuilder builder)
        where T : IHasPermissions<T>
    {
        var permissionIdGetterMethod = typeof(T).GetMethod(
            nameof(IHasPermissions<T>.PermissionIdGetter),
            BindingFlags.Static
            | BindingFlags.NonPublic
            | BindingFlags.Public
        );

        var idGetter = (Expression<Func<T, Guid>>)permissionIdGetterMethod!.Invoke(null, [])!;

        builder.Entity<T>()
            .Association(
                x => x.Permissions,
                idGetter,
                p => p.LinkId
            );
    }
}

public record User : IHasPermissions<User>, IBaseEntity
{
    public Guid Id { get; set; }

    public Guid MySpecialFieldForPermissionLinking { get; set; }

    public string Name { get; set; } = null!;

    // From IHasPermission<>
    public List<Permission> Permissions { get; set; } = null!;

    public static Expression<Func<User, Guid>> PermissionIdGetter()
        => x => x.MySpecialFieldForPermissionLinking;
}

public record Permission : IBaseEntity
{
    public Guid Id { get; set; }

    public Guid LinkId { get; set; }

    public string Grant { get; set; } = null!;
}

public interface IHasPermissions<T>
{
    // This is for versatility to choose based on what value permissions are linked
    static abstract Expression<Func<T, Guid>> PermissionIdGetter();

    List<Permission> Permissions { get; set; }
}

public static class IHasPermissionsExtensions
{
    public static IQueryable<T> OnlyWithPermissions<T>(this IQueryable<T> source)
        where T : IHasPermissions<T>
        => source.Where(x => x.Permissions.Any()); // Just to access it, in real life it's more complex
}

public interface IBaseEntity { }

Environment details

Linq To DB version: 5.4.1

Database (with version): SQLite

ADO.NET Provider (with version): System.Data.SQLite.Core 1.0.118

Operating system: Windows 11

.NET Version: 8.0

@joonatanu-softwerk joonatanu-softwerk changed the title Fluent mapper based association fails during query filter. Work when just accessing in Linq Fluent mapper + expression based association fails during query filter. Work when just accessing in Linq May 17, 2024
@joonatanu-softwerk joonatanu-softwerk changed the title Fluent mapper + expression based association fails during query filter. Work when just accessing in Linq Fluent mapper + expression based association fails during query filter. Works when just accessing in Linq May 17, 2024
@joonatanu-softwerk joonatanu-softwerk changed the title Fluent mapper + expression based association fails during query filter. Works when just accessing in Linq Fluent mapper + expression based association fails when using generic IQueryable extension. Works when just accessing in Linq May 17, 2024
@sdanyliv
Copy link
Member

This one is reproduced. I needs fix on linq2db side. I have made this fix, but I should prepare more tests.

@sdanyliv sdanyliv self-assigned this May 19, 2024
@sdanyliv sdanyliv added area: linq status: has-pr There is active PR for issue labels May 19, 2024
@MaceWindu MaceWindu added this to the 6.0.0-preview.1 milestone May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: linq status: has-pr There is active PR for issue
Development

Successfully merging a pull request may close this issue.

3 participants