从 EF Core 加载时计算 NotMapped 属性

2023-12-08

我们确实有一个实体类定义如下:

[Table("Users", Schema = "Mstr")]
[Audited]
public class User
{
    public virtual string FamilyName { get; set; }

    public virtual string SurName { get; set; }
    
    [NotMapped]
    public virtual string DisplayName
    {
        get => SurName + " " + FamilyName;
        private set { }
    }
}

这工作得很好。现在我们要提取逻辑部分SurName + " " + FamilyName到通常通过依赖注入注入的帮助器类。不幸的是 DI 不适用于实体类。

因此我的问题是:有什么方法可以拦截新 User 对象的创建吗?在 EF 创建 User 对象后,我是否可以重写 EF 中的方法来执行一些附加逻辑?


实际上(至少在 EF Core 6 中)您可以在构造实体时使用 DI。解决方案有点 hacky,并且基于 EF Core 功能inject“本机”服务(例如上下文本身)进入实体构造函数:

目前,只能注入 EF Core 已知的服务。正在考虑在未来版本中支持注入应用程序服务。

And AccessorExtensions.GetService<TService>扩展方法似乎支持从 DI 解析服务。

所以基本上只是介绍接受你的演员DbContext作为实体的参数并调用GetService并使用服务:

public class MyEntity
{
    public MyEntity()
    {        
    }

    public MyEntity(SomeContext context)
    {
        var valueProvider = context.GetService<IValueProvider>();
        NotMapped = valueProvider.GetValue();
    }

    public int Id { get; set; }

    [NotMapped]
    public string NotMapped { get; set; }
}


// Example value provider:
public interface IValueProvider
{
    string GetValue();
}

class ValueProvider : IValueProvider
{
    public string GetValue() => "From DI";
}

示例上下文:

public class SomeContext : DbContext
{
    public SomeContext(DbContextOptions<SomeContext> options) : base(options)
    {
    }

    public DbSet<MyEntity> Entities { get; set; }
}

例子:

var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IValueProvider, ValueProvider>();
serviceCollection.AddDbContext<SomeContext>(builder => 
    builder.UseSqlite($"Filename={nameof(SomeContext)}.db"));
var serviceProvider = serviceCollection.BuildServiceProvider();
// init db and add one item
using (var scope = serviceProvider.CreateScope())
{
    var someContext = scope.ServiceProvider.GetRequiredService<SomeContext>();
    someContext.Database.EnsureDeleted();
    someContext.Database.EnsureCreated();
    someContext.Add(new MyEntity());
    someContext.SaveChanges();
}

// check that value provider is used
using (var scope = serviceProvider.CreateScope())
{
    var someContext = scope.ServiceProvider.GetRequiredService<SomeContext>();
    var myEntities = someContext.Entities.ToList();
    Console.WriteLine(myEntities.First().NotMapped); // prints "From DI"
}

注意var valueProvider = context.GetRequiredService<IValueProvider>();如果服务未注册,则会抛出异常,因此下一个实现可能会更好:

public MyEntity(SomeContext context)
{
    var serviceProvider = context.GetService<IServiceProvider>();
    var valueProvider = serviceProvider.GetService<IValueProvider>();
    NotMapped = valueProvider?.GetValue() ?? "No Provider";
}

您还可以考虑删除未映射的属性,并使用它和服务创建单独的模型来执行映射。

同样在 EF Core 的第 7 版中new hook对于这种情况应该添加。看到这个github issue.

UPD。 EF Core 7 方法。

EF 7 添加了IMaterializationInterceptor(以及其他一些 - 请参阅docs) 正好可以用于此目标。因此更新后的代码可能如下所示:

不需要 ctor 接受实体中的上下文:

public class MyEntity
{
    public int Id { get; set; }

    [NotMapped]
    public string NotMapped { get; set; }
}

创建一个拦截器并重载它的方法之一(我选择了InitializedInstance):

class NotMappedValueGeneratingInterceptor : IMaterializationInterceptor
{
    public static NotMappedValueGeneratingInterceptor Instance = new ();
    public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
    {
        if (entity is MyEntity my)
        {
            var valueProvider = materializationData.Context.GetService<IValueProvider>();
            my.NotMapped = valueProvider.GetValue();
        }
        
        return entity;
    }
}

并使用我们的 DI 方法将拦截器添加到上下文设置中AddDbContext更改为:

serviceCollection.AddDbContext<SomeContext>(builder => 
    builder.UseSqlite($"Filename={nameof(SomeContext)}.db")
       .AddInterceptors(NotMappedValueGeneratingInterceptor.Instance));
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从 EF Core 加载时计算 NotMapped 属性 的相关文章

随机推荐