Skip to content

Upgrade from .NET 5 to .NET 6 - Using a custom IMethodCallTranslator no longer works #2186

@pafrench

Description

@pafrench

I've injected my own IMethodCallTranslatorProvider to effectively monkey patch in PostGIS methods in that aren't currently available via Npgsql. This was working as expected in .NET 5, but after upgrading to .NET 6 I get the following exception thrown:

System.InvalidCastException: Unable to cast object of type 'Imagery.Warehouse.Metadata.Data.EFExtensions.ExtraNpgsqlMethodCallTranslatorProvider' to type 'Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal.NpgsqlMethodCallTranslatorProvider'.
   at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlTranslatingExpressionVisitor..ctor(RelationalSqlTranslatingExpressionVisitorDependencies dependencies, QueryCompilationContext queryCompilationContext, QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlTranslatingExpressionVisitorFactory.Create(QueryCompilationContext queryCompilationContext, QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor..ctor(QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalQueryableMethodTranslatingExpressionVisitorFactory.Create(QueryCompilationContext queryCompilationContext)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.SingleOrDefault[TSource](IQueryable`1 source)
   at Imagery.Warehouse.Metadata.Data.Stores.ReadStore.GetImageset(String imagesetUrn) in /home/paul.french/src/imagery-warehouse-metadata/lib/Imagery.Warehouse.Metadata.Data/Stores/ReadStore.cs:line 57

Here's a brief overview of the implementing code:

public static class NpgsqlDbContextOptionsBuilderExtensions
{
    public static DbContextOptionsBuilder UseExtraFunctions(this DbContextOptionsBuilder optionsBuilder)
    {
        var extension = GetOrCreateExtension(optionsBuilder);
        ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

        return optionsBuilder;
    }
    
    private static NpgsqlDbContextOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.Options.FindExtension<NpgsqlDbContextOptionsExtension>()
           ?? new NpgsqlDbContextOptionsExtension();
}

public class NpgsqlDbContextOptionsExtension : IDbContextOptionsExtension
{
    private DbContextOptionsExtensionInfo _info;

    public void Validate(IDbContextOptions options)
    {
        // Nothing to do
    }

    public DbContextOptionsExtensionInfo Info {
        get
        {
            return _info ??= new MyDbContextOptionsExtensionInfo(this);
        }
    }

    public void ApplyServices(IServiceCollection services)
    {
        // services.AddSingleton<IMethodCallTranslatorProvider, ExtraNpgsqlMethodCallTranslatorProvider>();
        services.AddScoped<IMethodCallTranslatorProvider, ExtraNpgsqlMethodCallTranslatorProvider>();
    }

    private sealed class MyDbContextOptionsExtensionInfo : DbContextOptionsExtensionInfo
    {
        public MyDbContextOptionsExtensionInfo(IDbContextOptionsExtension instance) : base(instance) { }

        public override bool IsDatabaseProvider => false;

        public override string LogFragment => "";

        public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
        {
        }

        public override int GetServiceProviderHashCode() => 0;

        public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => false;
    }
}

public sealed class ExtraNpgsqlMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider
{
    public ExtraNpgsqlMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies) : base(dependencies)
    {
        var expressionFactory = dependencies.SqlExpressionFactory;
        AddTranslators(new List<IMethodCallTranslator>
        {
            new ExtraNpgsqlMethodCallTranslator(expressionFactory)
        });
    }
}

public static class ExtraNpgsqlDbFunctionsExtensions
{
    public static Geometry Force3D(this DbFunctions _, Geometry geometry) => throw new NotSupportedException();

    public static Geometry Union(this DbFunctions _, Geometry geometry) => throw new NotSupportedException();
    
    public static Geometry Multi(this DbFunctions _, Geometry geometry) => throw new NotSupportedException();
}

public class ExtraNpgsqlMethodCallTranslator : IMethodCallTranslator
{
    private readonly ISqlExpressionFactory _expressionFactory;

    private static readonly MethodInfo _force3DMethod
        = typeof(ExtraNpgsqlDbFunctionsExtensions).GetMethod(
            nameof(ExtraNpgsqlDbFunctionsExtensions.Force3D),
            new [] { typeof(DbFunctions), typeof(Geometry)});

    private static readonly MethodInfo _unionMethod
        = typeof(ExtraNpgsqlDbFunctionsExtensions).GetMethod(
            nameof(ExtraNpgsqlDbFunctionsExtensions.Union),
            new [] { typeof(DbFunctions), typeof(Geometry)});
    
    private static readonly MethodInfo _multiMethod
        = typeof(ExtraNpgsqlDbFunctionsExtensions).GetMethod(
            nameof(ExtraNpgsqlDbFunctionsExtensions.Multi),
            new [] { typeof(DbFunctions), typeof(Geometry)});

    public ExtraNpgsqlMethodCallTranslator(ISqlExpressionFactory expressionFactory)
    {
        _expressionFactory = expressionFactory;
    }

    public SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList<SqlExpression> arguments, IDiagnosticsLogger<DbLoggerCategory.Query> logger)
    {
        if (method == _force3DMethod)
        {
            return _expressionFactory.Function(
                "ST_Force3D",
                new[] {arguments[1]},
                nullable: true,
                new[] {true},
                method.ReturnType,
                arguments[1].TypeMapping);
        }

        if (method == _unionMethod)
        {
            return _expressionFactory.Function(
                "ST_Union",
                new[] { arguments[1] },
                true,
                new[] { true },
                method.ReturnType,
                arguments[1].TypeMapping);
        }
        
        if (method == _multiMethod)
        {
            return _expressionFactory.Function(
                "ST_Multi",
                new[] { arguments[1] },
                true,
                new[] { true },
                method.ReturnType,
                arguments[1].TypeMapping);
        }

        return null;
    }
}

Which is then registered/called in Startup.cs:

services.AddDbContext<ReadOnlyMetadataDbContext>((provider, builder) =>
{
    builder.UseNpgsql(_configuration.GetConnectionString(Data.Connection.Constants.ConnectionStringNames.Reader), o =>
    {
        o.UseNetTopologySuite();
        var databasePasswordManager = provider.GetRequiredService<IDatabasePasswordManager>();
        o.ProvidePasswordCallback(databasePasswordManager.GetReaderPassword);
    });
    builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    builder.UseExtraFunctions();
    if (isSecretManagerEnabled)
    {
        builder.AddInterceptors(provider.GetRequiredService<ReaderConnectionInterceptor>());
    }
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions