linq2db / linq2db.EntityFrameworkCore

Bring power of Linq To DB to Entity Framework Core projects
MIT License
449 stars 39 forks source link

Library broken with Projectables #364

Open AntonC9018 opened 9 months ago

AntonC9018 commented 9 months ago

I understand that this issue might be outside the scope and you might not care to support it. I'm trying to use the library in combination with this. It fails in the code that uses reflection to extract DbContext from IQueryable because of an invalid cast to QueryCompiler (that lib defines a custom query compiler). Here it is. Should we perhaps push the EF Core team to add API's for this? I guess a workaround that I could do is that I could override that method in a custom impl of that class, since the method's virtual.

AntonC9018 commented 9 months ago

Just tried making a hack to make it work:

#nullable enable
using System;
using System.Linq;
using System.Reflection;
using EntityFrameworkCore.Projectables.Infrastructure.Internal;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Internal;

#pragma warning disable EF1001 // Internal API use

// https://github.com/linq2db/linq2db.EntityFrameworkCore/issues/364
public sealed class LinqToDbEfCoreImplWithProjectableSupport : LinqToDBForEFToolsImplDefault
{
    public static readonly LinqToDbEfCoreImplWithProjectableSupport Instance = new();

    private static readonly FieldInfo QueryCompilerField;
    private static readonly FieldInfo QueryContextFactoryField;
    private static readonly Func<RelationalQueryContextFactory, QueryContextDependencies> GetDependenciesFunc;
    // https://github.com/koenbeuk/EntityFrameworkCore.Projectables/blob/4a8390565d5a8987ea6612c5c555afd35e2266dc/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs#L16
    private static readonly FieldInfo ProjectablesDecoratedQueryField;

    static LinqToDbEfCoreImplWithProjectableSupport()
    {
        FieldInfo GetPrivateField(Type type, string name)
        {
            var field = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
            if (field == null)
                throw new LinqToDBForEFToolsException($"Can't find {name} field.");
            return field;
        }
        QueryCompilerField = GetPrivateField(typeof(EntityQueryProvider), "_queryCompiler");
        QueryContextFactoryField = GetPrivateField(typeof(QueryCompiler), "_queryContextFactory");
        ProjectablesDecoratedQueryField = GetPrivateField(typeof(CustomQueryCompiler), "_decoratedQueryCompiler");

        {
            var dependenciesProperty = typeof(RelationalQueryContextFactory)
                .GetProperty("Dependencies", BindingFlags.NonPublic | BindingFlags.Instance)!
                ?? throw new LinqToDBForEFToolsException("Can't find Dependencies property.");
            var getDependenciesMethod = dependenciesProperty
                .GetMethod!
                ?? throw new LinqToDBForEFToolsException("Can't find Dependencies property getter.");
            GetDependenciesFunc = getDependenciesMethod
                .CreateDelegate<Func<RelationalQueryContextFactory, QueryContextDependencies>>();
        }
    }

    public override DbContext? GetCurrentContext(IQueryable query)
    {
        var compiler = QueryCompilerField.GetValue(query.Provider);
        while (true)
        {
            if (compiler is QueryCompiler efCoreQueryCompiler)
            {
                if (QueryContextFactoryField.GetValue(efCoreQueryCompiler) is not RelationalQueryContextFactory queryContextFactory)
                    throw new LinqToDBForEFToolsException("LinqToDB Tools for EFCore support only Relational Databases.");
                var dependencies = GetDependenciesFunc(queryContextFactory);
                return dependencies.CurrentContext.Context;
            }
            if (compiler is CustomQueryCompiler projectableCompiler)
            {
                compiler = ProjectablesDecoratedQueryField.GetValue(projectableCompiler);
                continue;
            }
            return null;
        }
    }
}

And the set that as the implementation in Main:

LinqToDBForEFTools.Initialize();
LinqToDBForEFTools.Implementation = LinqToDbEfCoreImplWithProjectableSupport.Instance;

However, it still kept its own instance? How do I make it work? Ideally Initialize should take a parameter with the impl, perhaps? For some reason the implementation property doesn't work.

MaceWindu commented 9 months ago

@sdanyliv , it looks to me that LinqToDBForEFTools.Implementation assignment should trigger re-initialize, which is not possible with current code due to Lazy already triggered.

AntonC9018 commented 9 months ago

@sdanyliv , it looks to me that LinqToDBForEFTools.Implementation assignment should trigger re-initialize, which is not possible with current code due to Lazy already triggered.

No, sorry, it works. I was instantiating the wrong class, that's why. I've fixed the code in the comment.