簡體   English   中英

如何為 shadow 屬性設置默認值

[英]How to set default values for shadow property

我有以下實體:

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覆蓋可以看出,我正在向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() 的默認值(請參閱默認值)以生成新行的值。 然后,您可以使用數據庫觸發器在更新期間生成值(例如以下示例觸發器)。

我不明白ValueGeneratedOnAddOrUpdate()的目的是什么,如果它的行為類似於ValueGeneratedOnAdd()並且我必須手動干預(創建觸發器)來設置這個屬性。

確實,如果我將Updated shadow 屬性的定義更改為

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

並覆蓋SaveChangesPersonDbContext

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- 創建一個標記為空的界面。 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與許多其他屬性一起使用,例如BrowserName userIp ...但我在本示例中刪除了這些屬性以使其盡可能簡單。 在這個例子中解釋一切並不容易,但如果你對這種方法有任何疑問,請隨時提問。

我發現討論中指出名稱ValueGeneratedOnAddOrUpdate()不夠描述。

@rowanmiller 感謝您的快速回復! 現在我知道它是如何工作的,但我認為這有點令人困惑。 命名“ValueGeneratedOnAddOrUpdate”表明值實際上是在插入和更新時生成的。

並且文檔說:“添加時生成的值”“添加時生成的值意味着如果您不指定值,則會為您生成一個值。”

“添加或更新時生成的值”“添加或更新時生成的值”表示每次保存記錄(插入或更新)時都會生成一個新值。

也許在文檔中添加一個部分來描述您如何提供自己的價值生成器?

但是, EF 團隊的建議是遵循警告部分並應用手動步驟來手動生成更新案例的值

@bcbeatty 詳細說明包含在主要部分中,Fluent 和數據注釋部分中有一個警告框,指向詳細注釋。

這只是讓 EF 知道值是為添加的實體生成的,並不保證 EF 會設置實際機制來生成值。 有關更多詳細信息,請參閱添加時生成的值部分。

所以看起來,至少對於DateTime陰影屬性,OP 中的方法是要走的路。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM