从 EF Core 加载时计算 NotMapped 属性

Calculate NotMapped property when loading from EF Core

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

[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 提取到助手 class 中,该助手通常使用依赖注入进行注入。不幸的是,DI 不适用于实体 class。

因此我的问题是:是否有任何方法可以拦截新用户对象的创建?是否有来自 EF 的方法,我可以覆盖它以在用户之后执行一些额外的逻辑对象是由 EF 创建的?

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

Currently, only services known by EF Core can be injected. Support for injecting application services is being considered for a future release.

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

所以基本上只是引入 ctor 接受你的 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.GetService<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 for exactly this case should be added. See this github 问题。

在您的 DbContext 或任何调用的上下文文件中,您可以拦截 SaveChanges() 方法并用您自己的东西覆盖它。在我的示例中,我覆盖了 SaveChanges() 以自动添加我的审计字段,因此我不必在整个代码中的一百万个地方复制它。

这是我的例子。因此,当创建一个新对象时,您可以覆盖它。在我的示例中,我覆盖了添加的新记录和修改的记录。

这些标记在 EntitState.Added 和 EntityStateModified。

这是代码。

public override int SaveChanges()
        {

            var state = this.ChangeTracker.Entries().Select(x => x.State).ToList();

            state.ForEach(x => {
                if (x == EntityState.Added)
                {
                    //Create new record changes
                    var created = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Added).Select(e => e.Entity).ToArray();

                    foreach (var entity in created)
                    {
                        if (entity is AuditFields)
                        {
                            var auditFields = entity as AuditFields;
                            auditFields.CreateDateTimeUtc = DateTime.UtcNow;
                            auditFields.ModifiedDateTimeUtc = DateTime.UtcNow;
                            auditFields.Active = true;
                        }
                    }
                }

                else if (x == EntityState.Modified)
                {
                    //Modified record changes
                    var modified = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified).Select(e => e.Entity).ToArray();

                    foreach (var entity in modified)
                    {
                        if (entity is AuditFields)
                        {
                            var auditFields = entity as AuditFields;
                            auditFields.ModifiedDateTimeUtc = DateTime.UtcNow;
                        }
                    }

                }
                else
                {
                    //do nothing
                }

            });

            return base.SaveChanges();
        }

既然你说了:

is there any way to intercept the creation of new User objects?

您可能希望在上面代码的 EntityState.Added 区域执行您的逻辑,这将允许您拦截新用户的创建并在将其保存到数据库之前执行任何您想执行的操作。