如何为阴影属性设置默认值

3
我有以下实体:
public class Person
{
    public Guid Id { get; set; }

    public string Name { get; set; }
}

这是我的数据库上下文。
public class PersonDbContext : DbContext
{
    private static readonly ILoggerFactory
        Logger = LoggerFactory.Create(x => x.AddConsole());

    public DbSet<Person> Persons { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseLoggerFactory(Logger)
            .UseSqlServer(
                "Server=(localdb)\\mssqllocaldb;Database=PersonDb;Trusted_Connection=True;MultipleActiveResultSets=true");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Person>()
            .Property<DateTime>("Created")
            .HasDefaultValueSql("GETUTCDATE()")
            .ValueGeneratedOnAdd();

        modelBuilder
            .Entity<Person>()
            .Property<DateTime>("Updated")
            .HasDefaultValueSql("GETUTCDATE()")
            .ValueGeneratedOnAddOrUpdate();
    }
}

OnModelCreating 重写可以看出,我正在将影子属性 Updated/Created 添加到 Person 实体中。
我设置这些属性以填充 SQL 默认值:
  • 当添加值时,Created
  • 当添加或更新值时,Updated
下面是客户端代码。
var personId = Guid.Parse("CF5EE27D-C694-408A-9F7B-080FF6315843");

using (var dbContext = new PersonDbContext())
{
    var person = new Person
    {
        Id = personId,
        Name = "New Person"
    };

    dbContext.Add(person);

    await dbContext.SaveChangesAsync();
}

using (var dbContext = new PersonDbContext())
{
    var person = dbContext.Persons.Find(personId);

    var personName = person.Name;

    person.Name = $"{personName} {DateTime.UtcNow}";

    dbContext.SaveChanges();
}

我可以确认,在插入新人员时,两个属性均设置为UTC日期。 然而,在更新时,“Updated”属性未被设置。
以下是生成的t-sql代码:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[@p1='?' (DbType = Guid), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [Persons] SET [Name] = @p0
      WHERE [Id] = @p1;
      SELECT [Updated]
      FROM [Persons]
      WHERE @@ROWCOUNT = 1 AND [Id] = @p1;

阅读添加或更新时生成值的文档,我看到以下警告:

但是,如果您指定DateTime属性在添加或更新时生成,则必须设置一种方法来生成值。一种方法是配置默认值GETDATE()(请参见默认值)以为新行生成值。然后,您可以使用数据库触发器在更新期间生成值(例如以下示例触发器)。

那么,如果它的行为类似于ValueGeneratedOnAdd()并且我必须手动干预(创建触发器)来设置此属性,那么ValueGeneratedOnAddOrUpdate()的目的是什么?

确实,如果我更改Updated影子属性的定义为

modelBuilder
    .Entity<Person>()
    .Property<DateTime>("Updated")
    .HasDefaultValueSql("GETUTCDATE()")
    .ValueGeneratedOnAdd();

PersonDbContext上覆盖SaveChanges方法。
public override int SaveChanges()
{
    ChangeTracker.DetectChanges();

    foreach (var entry in ChangeTracker.Entries().Where(entity => entity.State == EntityState.Modified))
    {
        entry.Property("Updated").CurrentValue = DateTime.UtcNow;
    }

    return base.SaveChanges();
}

这做了预期的事情。
因此问题是 - 在 EF Core 中设置阴影属性的默认值的正确方式是什么。
这是我大型项目中的简化示例,因此在 OnModelCreating 覆盖中对实体使用 HasData 不是一个好选择(由于有许多实体)。
我正在使用 EF Core 3.1.1。
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1"/>

1
也许这个链接会有所帮助? - Trevor
2个回答

0

我在讨论区找到了一篇文章,指出ValueGeneratedOnAddOrUpdate()这个名称不够描述性。

@rowanmiller 谢谢你的快速回复!现在我知道它是如何工作的,但是我觉得有点令人困惑。命名为"ValueGeneratedOnAddOrUpdate"暗示着在插入和更新时实际上会生成一个值。

而文档中说:"在添加时生成的值","在添加时生成的值意味着如果您没有指定一个值,系统会为您生成一个值。"

"在添加或更新时生成的值","在添加或更新时生成的值意味着每次保存记录(插入或更新)时都会生成一个新值。"

也许可以在文档中添加一个章节,介绍如何提供自己的值生成器?

然而,EF团队的建议是遵循警告部分并手动应用步骤来手动生成更新案例的值

@bcbeatty详细的解释已包含在主要部分中, 在流畅和数据注释部分有一个注意事项框, 指向详细说明。

这只是让EF知道为添加的实体生成了值, 它并不保证EF会设置实际的机制来 生成值。有关更多详细信息,请参阅添加时生成的值部分。

因此,对于DateTime的影子属性,似乎OP的方法是正确的。


0
如果你想要有可重复使用的阴影属性,请按照以下步骤进行操作。
1- 创建一个空标记接口。 IAuditableEntity.cs
    /// <summary>
    /// It's a marker interface, in order to make our entities audit-able.
    /// Every entity you mark with this interface, will save audit info to the database.
    /// </summary>
    public interface IAuditableEntity
    { }

2- 创建一个静态类来编写您的影子属性逻辑。AuditableShadowProperties.cs

public static class AuditableShadowProperties {

    public static readonly Func<object, DateTimeOffset?> EfPropertyCreatedDateTime =
        entity => EF.Property<DateTimeOffset?> (entity, CreatedDateTime);

    public static readonly string CreatedDateTime = nameof (CreatedDateTime);

    public static readonly Func<object, DateTimeOffset?> EfPropertyModifiedDateTime =
        entity => EF.Property<DateTimeOffset?> (entity, ModifiedDateTime);

    public static readonly string ModifiedDateTime = nameof (ModifiedDateTime);

    public static void AddAuditableShadowProperties (this ModelBuilder modelBuilder) {
        foreach (var entityType in modelBuilder.Model
                .GetEntityTypes ()
                .Where (e => typeof (IAuditableEntity).IsAssignableFrom (e.ClrType))) {
            modelBuilder.Entity (entityType.ClrType)
                .Property<DateTimeOffset?> (CreatedDateTime);

            modelBuilder.Entity (entityType.ClrType)
                .Property<DateTimeOffset?> (ModifiedDateTime);

        }
    }

    public static void SetAuditableEntityPropertyValues (
        this ChangeTracker changeTracker) {
        var now = DateTimeOffset.UtcNow;

        var modifiedEntries = changeTracker.Entries<IAuditableEntity> ()
            .Where (x => x.State == EntityState.Modified);
        foreach (var modifiedEntry in modifiedEntries) {
            modifiedEntry.Property (ModifiedDateTime).CurrentValue = now;
        }

        var addedEntries = changeTracker.Entries<IAuditableEntity> ()
            .Where (x => x.State == EntityState.Added);
        foreach (var addedEntry in addedEntries) {
            addedEntry.Property (CreatedDateTime).CurrentValue = now;
        }
    }
}

3- 将必要的更改添加到您的PersonDbContext以使用您的IAuditableEntity
 // first we add our shadow properties to the database with next migration
 protected override void OnModelCreating(ModelBuilder builder)
{
...
  builder.AddAuditableShadowProperties();
}

// override saveChanges methods to use our shadow properties.
        public override int SaveChanges()
        {
            ChangeTracker.DetectChanges();

            BeforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled =
                false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges();
            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

     public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
        {
            ChangeTracker.DetectChanges();

            BeforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled =
                false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChangesAsync(cancellationToken);
            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
            CancellationToken cancellationToken = new CancellationToken())
        {
            ChangeTracker.DetectChanges();

            BeforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled =
                false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        #region "ExtraMethods"

        public T GetShadowPropertyValue<T>(object entity, string propertyName) where T : IConvertible
        {
            var value = this.Entry(entity).Property(propertyName).CurrentValue;
            return value != null ?
                (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture) :
                default(T);
        }

        public object GetShadowPropertyValue(object entity, string propertyName)
        {
            return this.Entry(entity).Property(propertyName).CurrentValue;
        }


        private void BeforeSaveTriggers()
        {
            ValidateEntities();
            SetShadowProperties();
        }

        private void ValidateEntities()
        {
            var errors = this.GetValidationErrors();
            if (!string.IsNullOrWhiteSpace(errors))
            {
                // we can't use constructor injection anymore, because we are using the `AddDbContextPool<>`
                var loggerFactory = this.GetService<ILoggerFactory>();
                loggerFactory.CheckArgumentIsNull(nameof(loggerFactory));
                var logger = loggerFactory.CreateLogger<AppDbContext>();
                logger.LogError(errors);
                throw new InvalidOperationException(errors);
            }
        }

        private void SetShadowProperties()
        {
            ChangeTracker.SetAuditableEntityPropertyValues();
        }
        #endregio

用法:

4- 现在你可以将 IAuditableEntity 接口添加到任何你想要具有这些阴影属性的实体上,完成后就可以了。

public class Person : IAuditableEntity
{
    public Guid Id { get; set; }

    public string Name { get; set; }
}

我正在使用IAuditableEntity,其中包含许多其他属性,例如BrowserNameuserIp...但在这个示例中,我将它们移除以保持尽可能简单。在这个示例中不容易解释所有内容,如果对这种方法有任何问题,请随时提问。


感谢您分享代码。在我的特定情况下,在OnModelCreating中添加阴影属性就足够了,无需在接口中定义它们,因为所有实体都应该具有这些属性(Created和Updated)。 - Michael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接