dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.48k stars 3.13k forks source link

Materialization interception for non-entity types #33157

Open AllanMichaelsen opened 4 months ago

AllanMichaelsen commented 4 months ago

I added an IMaterializationInterceptor like below (the exceptions are for this purpose only):

public class MaterializationInterceptor : IMaterializationInterceptor
{
    private MaterializationInterceptor() { }

    public object CreatedInstance(MaterializationInterceptionData materializationData, object entity)
    {
        throw new Exception("CreatedInstance");
    }

    public InterceptionResult<object> CreatingInstance(MaterializationInterceptionData materializationData, InterceptionResult<object> result)
    {
        throw new Exception("CreatingInstance");
    }

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        throw new Exception("InitializedInstance");
    }

    public InterceptionResult InitializingInstance(MaterializationInterceptionData materializationData, object entity, InterceptionResult result)
    {
        throw new Exception("InitializingInstance");
    }

    public static MaterializationInterceptor Instance { get; } = new();
}

Added to my contect as:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    base.OnConfiguring(optionsBuilder);

     optionsBuilder.AddInterceptors(
        MaterializationInterceptor.Instance,
        DbCommandInterceptor.Instance
    );
}

When I run a query against my context I didn't get the expected result from the value substitution I have in the "InitializedInstance" method. During debugging I can see that the constructor is hit, but my breakpoints are never hit. So for some reason the methods (any of the four) are never hit.

I also have a "DbCommandInterceptor" and a "SaveChangesInterceptor". Both of those work fine.

EF Core version: 8,0,2 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8.0 Operating system: Windows 11 Pro IDE: Visual Studio 2022 17.9.0 Preview 4

ajcvickers commented 4 months ago

@AllanMichaelsen I am not able to reproduce this--see my code below. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

using (var context = new SomeDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(new SubscriptionPlan());
    await context.SaveChangesAsync();
}

using (var context = new SomeDbContext())
{
    var results = await context.SubscriptionPlans.ToListAsync();
}

public class SomeDbContext : DbContext
{
    public DbSet<SubscriptionPlan> SubscriptionPlans => Set<SubscriptionPlan>();

    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)
            .AddInterceptors(MaterializationInterceptor.Instance)
            .EnableSensitiveDataLogging();
}

public class SubscriptionPlan
{
    public int Id { get; set; }
    public bool IsFree { get; set; }
    public string? Bar { get; set; }
}

public class MaterializationInterceptor : IMaterializationInterceptor
{
    private MaterializationInterceptor() { }

    public object CreatedInstance(MaterializationInterceptionData materializationData, object entity)
    {
        throw new Exception("CreatedInstance");
    }

    public InterceptionResult<object> CreatingInstance(MaterializationInterceptionData materializationData, InterceptionResult<object> result)
    {
        throw new Exception("CreatingInstance");
    }

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        throw new Exception("InitializedInstance");
    }

    public InterceptionResult InitializingInstance(MaterializationInterceptionData materializationData, object entity, InterceptionResult result)
    {
        throw new Exception("InitializingInstance");
    }

    public static MaterializationInterceptor Instance { get; } = new();
}
AllanMichaelsen commented 4 months ago

When creating the demo project, the interceptor was hit. I tried again to dig into the difference between my project and the demo project and I did find the culprit.

I use AutoMapperin my project and to be more specific I use the method: .ProjectTo<TOut>(mapper.ConfigurationProvider) as seen below.

if (typeof(TOut) == typeof(T))
{
    return await queryable
      .Cast<TOut>()
      .FirstOrDefaultAsync();
}

 return await queryable
   .ProjectTo<TOut>(mapper.ConfigurationProvider)
   .FirstOrDefaultAsync();

So when I request TOut to be of my entity type the Interceptor is hit, but when I request my mapped Dto object, it is not. It seems that AutoMapper does not utilize the "InitializedInstance" (or any) method in the IMaterializationInterceptor.

The question is now. Is there a solution to this or do I have to rethink my mapping setup (I hope not)

ajcvickers commented 4 months ago

@AllanMichaelsen The materialization interceptor only kicks in for the materialization of entity type instances, not arbitrary projected types.

Note for team triage: it could be useful to have an interceptor for creation of non-entity types.

AllanMichaelsen commented 4 months ago

Noted. I have made a temporary fix via my Automapper projection and await an update for an non-entity inceptor.

Wil such update be posted here in this thread? Both if it is put in and in case it is decided not implement the feature?

ajcvickers commented 4 months ago

@AllanMichaelsen If we decide it is worthwhile, then the issue will go on the backlog and be prioritized accordingly. Realistically, it's unlikely to happen any time soon.