Open chrisdreams13 opened 7 months ago
Full repro below:
using (var context = new SomeDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Add(new Foo());
context.SaveChanges();
context.Database.ExecuteSqlRaw(
"""
CREATE FUNCTION dbo.Test(@i int) RETURNS int AS
begin
return @i;
end
"""
);
}
using (var context = new SomeDbContext())
{
Console.WriteLine(context.Set<Foo>().Select(e => context.Test(e.Id)).FirstOrDefault());
}
using (var context = new SomeDbContext())
{
WithInterface(context, context);
}
void WithInterface(SomeDbContext context1, IMyDbContext context2)
{
Console.WriteLine(context1.Foos.Select(e => context2.Test(e.Id)).FirstOrDefault());
}
public class SomeDbContext : DbContext, IMyDbContext
{
public DbSet<Foo> Foos => Set<Foo>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Data Source=localhost;Database=One;Integrated Security=True;Trust Server Certificate=True")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
[DbFunction("Test", "dbo", IsBuiltIn = false)]
public int Test(int i) => throw new NotSupportedException();
}
public class Foo
{
public int Id { get; set; }
}
public interface IMyDbContext
{
int Test(int i);
}
This also happens when inheriting SomeDbContext class and overriding parent's virtual method with the annotation.
See the gist for fuller repro:
https://gist.github.com/chris-pie/5c7a2a2215947b1a55ac9f7d36829c8b
This seems to be a result of a quirk? (I'm not informed enough to say it's a bug) in the compiler itself. The following line
Expression<Func<string>> selector = () => context.Test(1);
Produces following IL
IL_00c0: ldloc.1 // 'CS$<>8__locals0'
IL_00c1: ldtoken Program/'<>c__DisplayClass0_0'
IL_00c6: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_00cb: call class [System.Linq.Expressions]System.Linq.Expressions.ConstantExpression [System.Linq.Expressions]System.Linq.Expressions.Expression::Constant(object, class [System.Runtime]System.Type)
IL_00d0: ldtoken field class InheritingDbContext Program/'<>c__DisplayClass0_0'::context
IL_00d5: call class [System.Runtime]System.Reflection.FieldInfo [System.Runtime]System.Reflection.FieldInfo::GetFieldFromHandle(valuetype [System.Runtime]System.RuntimeFieldHandle)
IL_00da: call class [System.Linq.Expressions]System.Linq.Expressions.MemberExpression [System.Linq.Expressions]System.Linq.Expressions.Expression::Field(class [System.Linq.Expressions]System.Linq.Expressions.Expression, class [System.Runtime]System.Reflection.FieldInfo)
IL_00df: ldtoken method instance string SomeDbContext::Test(int32)
IL_00e4: call class [System.Runtime]System.Reflection.MethodBase [System.Runtime]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [System.Runtime]System.RuntimeMethodHandle)
IL_00e9: castclass [System.Runtime]System.Reflection.MethodInfo
And similarily for using the object as interface:
IL_0035: ldloc.0 // 'CS$<>8__locals0'
IL_0036: ldtoken Program/'<>c__DisplayClass0_4'
IL_003b: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_0040: call class [System.Linq.Expressions]System.Linq.Expressions.ConstantExpression [System.Linq.Expressions]System.Linq.Expressions.Expression::Constant(object, class [System.Runtime]System.Type)
IL_0045: ldtoken field class IMyDbContext Program/'<>c__DisplayClass0_4'::contextInterface
IL_004a: call class [System.Runtime]System.Reflection.FieldInfo [System.Runtime]System.Reflection.FieldInfo::GetFieldFromHandle(valuetype [System.Runtime]System.RuntimeFieldHandle)
IL_004f: call class [System.Linq.Expressions]System.Linq.Expressions.MemberExpression [System.Linq.Expressions]System.Linq.Expressions.Expression::Field(class [System.Linq.Expressions]System.Linq.Expressions.Expression, class [System.Runtime]System.Reflection.FieldInfo)
IL_0054: ldtoken method instance string IMyDbContext::Test(int32)
IL_0059: call class [System.Runtime]System.Reflection.MethodBase [System.Runtime]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [System.Runtime]System.RuntimeMethodHandle)
IL_005e: castclass [System.Runtime]System.Reflection.MethodInfo
See that it is not getting methodInfo of the object's actual class, but either from the interface or the parent class in case of virtual methods. What this means is that by the time any EFCore function is called we already can't tell which particular method was actually called and so we can't reliably map it to a DB function. I've implemented a workaround for this although it means manually fixing the parameter type in the Funcletizer (since by the time we get to translation original type is gone from the expression tree). This seems to work and doesn't break any existing tests but is probably not the ideal approach. Maybe instead we could replace the method instead of the type in the funcletizer using the same logic, or pass the original object's type to query compilation context from the funcletizer. Also might be worth pinging runtime/compiler teams with this,
DbFunction don't get the mapping function when an interface is used
There's a strange behavior when an interface is used instead of the direct object of dbcontext when a DbFunciton is called. In the code bellow, I create a function in sql (Test) and added it to my context using DbFunctionAttribute.
If MyDbContext is used to call the Test function works ok. The problem is when the IMyDbContext interface is required to the dependency injection, the Test function throws the NotSupportedException like the mapping doesn't exist.
I tried to cast the interface reference to the base class and works ok, but I believe it's not the desired behavior using interfaces. Also, I tried to add virtual to Test function in MyDbContext, but didn't works with the interface.
Code
Stack traces
Include provider and version information
EF Core version: Microsoft.EntityFrameworkCore, 8.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer, 8.0.0 Target framework: net8.0 Operating system: Windows 10 IDE: Microsoft Visual Studio Professional 2022 (64-bit) 17.8.3