如何在数据库优先方法中设置创建日期和修改日期

问题描述 投票:0回答:9

在我们应用程序的每个 SQL 表中,我们都有“CreatedDate”和“ModifiedDate”列。我们使用数据库优先的方法。当我保存数据时,我希望自动填充这两列。一种方法是在 SQL 表列本身上将默认值设置为

getdate()
。这样就部分解决了问题。这意味着当实体是新的时它将设置 CreatedDate 和 ModifiedDate。 但是,当我编辑/更新实体时,我只想更新
ModifiedDate

有很多文章使用代码优先方法来做到这一点。但我们使用的是数据库优先方法。
我在这里有什么选择?

entity-framework ef-database-first
9个回答
26
投票

请参阅此答案

.NET Core

https://stackoverflow.com/a/64824067/3850405

我喜欢这样使用更通用的方法:

interface IEntityDate
{
    DateTime CreatedDate { get; set; }

    DateTime UpdatedDate { get; set; }
}

public abstract class EntityBase<T1>: IEntityDate
{
    public T1 Id { get; set; }

    public virtual DateTime CreatedDate { get; set; }
    public virtual string CreatedBy { get; set; }
    public virtual DateTime UpdatedDate { get; set; }
    public virtual string UpdatedBy { get; set; }
}

public override int SaveChanges()
{
    var now = DateTime.UtcNow;

    foreach (var changedEntity in ChangeTracker.Entries())
    {
        if (changedEntity.Entity is IEntityDate entity)
        {
            switch (changedEntity.State)
            {
                case EntityState.Added:
                    entity.CreatedDate = now;
                    entity.UpdatedDate = now;
                    break;

                case EntityState.Modified:
                    Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                    entity.UpdatedDate = now;
                    break;
            }
        }
    }

    return base.SaveChanges();
}

更新:

为了处理

CreatedBy
UpdatedBy
,我使用 DbContext 的包装器,如下所示:

public interface IEntity
{
    DateTime CreatedDate { get; set; }

    string CreatedBy { get; set; }

    DateTime UpdatedDate { get; set; }

    string UpdatedBy { get; set; }
}

public interface ICurrentUser
{
    string GetUsername();
}

public class ApplicationDbContextUserWrapper
{
    public ApplicationDbContext Context;

    public ApplicationDbContextUserWrapper(ApplicationDbContext context, ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        this.Context = context;
    }
}

public class ApplicationDbContext : DbContext
{

    public ICurrentUser CurrentUser;
    
    public override int SaveChanges()
    {
        var now = DateTime.UtcNow;

        foreach (var changedEntity in ChangeTracker.Entries())
        {
            if (changedEntity.Entity is IEntity entity)
            {
                switch (changedEntity.State)
                {
                    case EntityState.Added:
                        entity.CreatedDate = now;
                        entity.UpdatedDate = now;
                        entity.CreatedBy = CurrentUser.GetUsername();
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                    case EntityState.Modified:
                        Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                        Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                        entity.UpdatedDate = now;
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                }
            }
        }

        return base.SaveChanges();
    }
    
    ...

20
投票

对于那些使用异步系统(SaveChangesAsync)和.net core的人最好将以下代码添加到DbContext类中。

    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        var AddedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Added).ToList();

        AddedEntities.ForEach(E =>
        {
            E.Property("CreationTime").CurrentValue = DateTime.Now;
        });

        var EditedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Modified).ToList();

        EditedEntities.ForEach(E =>
        {
            E.Property("ModifiedDate").CurrentValue = DateTime.Now;
        });

        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

您也可以定义一个类或接口,如下所示。

public class SaveConfig
{
    public DateTime CreationTime { get; set; }
    public DateTime? ModifiedDate { get; set; }
}

只需从 SaveConfig 实体继承实体。


20
投票

如果您想重写 OnSave,则必须重写所有保存方法。在 EF core 2.1 中,您可以使用 ChangeTracked 和事件更好的解决方案。例如:

您可以创建接口或基类,如下例所示:

public interface IUpdateable 
{ 
    DateTime ModificationDate{ get; set; }
}

public class SampleEntry : IUpdateable
{
    public int Id { get; set; }
    public DateTime ModificationDate { get; set; }
} 

然后在上下文创建时将事件添加到更改跟踪器:

context.ChangeTracker.StateChanged += context.ChangeTracker_StateChanged;

及方法:

private void ChangeTracker_StateChanged(object sender, Microsoft.EntityFrameworkCore.ChangeTracking.EntityStateChangedEventArgs e)
    {
        if(e.Entry.Entity is IUpdateable && e.Entry.State == EntityState.Modified)
        {
            var entry = ((IUpdateable)e.Entry.Entity);
            entry.ModificationDate = DateTime.Now;
        }
    }

更简单,您不必重写所有方法。


10
投票

您可以重写 DbContext 中的 SaveChanges 方法并获取已添加和已修改实体的列表,如果添加了则设置 CreatedDate,如果修改了则设置 ModifiedDate。

public override int SaveChanges()
{
    var AddedEntities = ChangeTracker.Entries<Entity>().Where(E => E.State == EntityState.Added).ToList();

    AddedEntities.ForEach(E => 
    {
        E.CreatedDate = DateTime.Now;
    });

    var ModifiedEntities = ChangeTracker.Entries<Entity>().Where(E => E.State == EntityState.Modified).ToList();

    ModifiedEntities.ForEach(E => 
    {
        E.ModifiedDate = DateTime.Now;
    });

    return base.SaveChanges();
}

您还可以编写一个具有两个 DateTime 属性的接口,并使您的实体继承它们。

interface IEntityDate
{
    DateTime AddedDate { set; get;}
    DateTime ModifiedDate { set; get;}
}

class Entity : IEntityDate
{
    public DateTime AddedDate { set; get;}
    public DateTime ModifiedDate { set; get;}
}

5
投票

从 EF-Core 5x 开始,您可以在

DbContext
之外使用拦截器,而不是重写
SaveChanges
方法。

其一般格式如下所示:

public class AuditableEntitiesInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        if (eventData == null)
        {
            throw new ArgumentNullException(nameof(eventData));
        }

        BeforeSaveTriggers(eventData.Context);
        return result;
    }

     public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData == null)
        {
            throw new ArgumentNullException(nameof(eventData));
        }

        BeforeSaveTriggers(eventData.Context);
        return ValueTask.FromResult(result);
    }

    private void BeforeSaveTriggers(DbContext? context)
    {
        var entries = context?.ChangeTracker
                .Entries()
                .Where(e => e.Entity is BaseEntity && (
                     e.State == EntityState.Added
                     || e.State == EntityState.Modified));

        foreach (var entityEntry in entries)
        {
            ((BaseEntity)entityEntry.Entity).UpdatedDate = DateTimeOffset.UtcNow;

        if (entityEntry.State == EntityState.Added)
        {
            ((BaseEntity)entityEntry.Entity).CreatedDate = DateTimeOffset.UtcNow;
        }
    }
}

}

您可以在

ChangeTracker
方法中使用
BeforeSaveTriggers
来跟踪实体更改。

由于该拦截器使用依赖注入,所以需要将其注册为服务。

services.AddSingleton<AuditableEntitiesInterceptor>();

然后可以在程序启动时从依赖注入中取出并引入到上下文中。

services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
                optionsBuilder
                    .UseSqlServer(connectionString)
                    .AddInterceptors(serviceProvider.GetRequiredService<AuditableEntitiesInterceptor>()));

值得注意的是,此示例

BaseEntity
使用
DateTimeOffset
代替
DateTime
以实现更好的本地化,您可以根据需要调整您的实现。

 public class BaseEntity{ 
    public DateTimeOffset CreatedDate { get; set; }
    public DateTimeOffset UpdatedDate { get; set; }    
}

3
投票

我做了与 @Arash 提出的公共覆盖“Task SaveChangesAsync”类似的事情,不同之处在于,在我的设置中,我只想更新实际具有 CreatedDate/ModifiedDate 属性的实体,这就是我想出的。与@Arash非常相似,但也许它可以帮助某人。 添加了几个常量 EntityCreatedPropertyName 和 EntityModifiedPropertyName 定义属性的名称,这样我就不必在使用它们的地方使用重复的字符串。

    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        #region Automatically set "CreatedDate" property on an a new entity
        var AddedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Added).ToList();

        AddedEntities.ForEach(E =>
        {
            var prop = E.Metadata.FindProperty(EntityCreatedPropertyName);
            if (prop != null)
            {
                E.Property(EntityCreatedPropertyName).CurrentValue = DateTime.Now;
            }
        });
        #endregion

        #region Automatically set "ModifiedDate" property on an a new entity
        var EditedEntities = ChangeTracker.Entries().Where(E => E.State == EntityState.Modified).ToList();

        EditedEntities.ForEach(E =>
        {
            var prop = E.Metadata.FindProperty(EntityModifiedPropertyName);
            if (prop != null)
            {
                E.Property(EntityModifiedPropertyName).CurrentValue = DateTime.Now;
            }
        });
        #endregion

        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

2
投票

这是我在 EF Core 2.1 中解决的方法:

我有一个基本模型,它继承了类似于以下的接口:

public interface IAuditableModel
{
    DateTime Created { get; }
    DateTime? Updated { get; set; }
}

public class BaseModel : IAuditableModel
{
    public BaseModel()
    {
    }

    public int Id { get; set; }

    public DateTime Created { get; private set; }

    public DateTime? Updated { get; set; }
}

然后,在我的 DbContext 中:

public override int SaveChanges()
    {
        var added = ChangeTracker.Entries<IAuditableModel>().Where(E => E.State == EntityState.Added).ToList();

        added.ForEach(E =>
        {
            E.Property(x => x.Created).CurrentValue = DateTime.UtcNow;
            E.Property(x => x.Created).IsModified = true;
        });

        var modified = ChangeTracker.Entries<IAuditableModel>().Where(E => E.State == EntityState.Modified).ToList();

        modified.ForEach(E =>
        {
            E.Property(x => x.Updated).CurrentValue = DateTime.UtcNow;
            E.Property(x => x.Updated).IsModified = true;

            E.Property(x => x.Created).CurrentValue = E.Property(x => x.Created).OriginalValue;
            E.Property(x => x.Created).IsModified = false;
        });

        return base.SaveChanges();
    }

这使您可以避免对任何属性名称进行硬编码,并可以灵活地将特定接口应用于您选择的模型。就我而言,我希望在大多数模型上使用此功能,因此我将其应用于所有其他模型都继承自的 BaseModel。

我还发现在编辑这些模型的视图中,创建的时间戳被擦除,因为我没有将其加载到页面上。因此,当模型在回发时绑定时,该属性被删除,将我创建的时间戳有效设置为 0。


1
投票

这是一种更通用的方法。

像这样配置实体: 不要忘记添加“SetAfterSaveBehavior AND SetBeforeSaveBehavior”以避免插入空值引发异常

builder.Property(p => p.ModifiedDate)
  .HasValueGenerator<DateTimeGenerator>()
  .ValueGeneratedOnAddOrUpdate()
  .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);            
builder.Property(p => p.ModifiedDate)
  .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Save);

如何编写 DateTimeGenerator 类:

internal class DateTimeGenerator : ValueGenerator<DateTime>
{
    public override bool GeneratesTemporaryValues => false;

    public override DateTime Next(EntityEntry entry)
    {
        if (entry is null)
        {
            throw new ArgumentNullException(nameof(entry));
        }

        return DateTime.Now;
    }
}

如何编写DbContext类

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
  GenerateOnUpdate();
  return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(
  bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
  {
    GenerateOnUpdate();
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
  }

  private void GenerateOnUpdate()
  {
    foreach (EntityEntry entityEntry in ChangeTracker.Entries())
    {
      foreach (PropertyEntry propertyEntry in entityEntry.Properties)
      {
        IProperty property = propertyEntry.Metadata;
        Func<IProperty, IEntityType, ValueGenerator> valueGeneratorFactory = property.GetValueGeneratorFactory();
        bool generatedOnUpdate = (property.ValueGenerated & ValueGenerated.OnUpdate) == ValueGenerated.OnUpdate;
        
        if (!generatedOnUpdate || valueGeneratorFactory == null)
        {
          continue;
        }

        ValueGenerator valueGenerator = valueGeneratorFactory.Invoke(
          property,
          entityEntry.Metadata
        );                    
        propertyEntry.CurrentValue = valueGenerator.Next(entityEntry);
      }
    }
  }

0
投票

Kahbazi 答案的变体,但使用 nameof() 而不是属性名称的魔术值。根据您的用例,您可能需要将

DateTime.Now
更改为
DateTime.UtcNow

public void Save() {
    var entitiesToCreate = _context.ChangeTracker.Entries().Where(entity => entity.State == EntityState.Added).ToList();
    var entitiesToUpdate = _context.ChangeTracker.Entries().Where(entity => entity.State == EntityState.Modified).ToList();

    entitiesToCreate.ForEach(entity => entity.Property(nameof(Entity.CreatedDate)).CurrentValue = DateTime.Now);
    entitiesToUpdate.ForEach(entity => entity.Property(nameof(Entity.UpdatedDate)).CurrentValue = DateTime.Now);

    _context.SaveChanges();
}

底层实体如下所示:

public abstract class Entity : IEntity {
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int? Id { get; set; }

    public DateTime CreatedDate { get; set; }
    public DateTime? UpdatedDate { get; set; }

    [Required]
    public bool Deleted { get; set; } = false;

    // ConcurrencyToken
    [Timestamp]
    public byte[]? RowVersion { get; set; } = BitConverter.GetBytes(DateTime.Now.ToBinary());
}
© www.soinside.com 2019 - 2024. All rights reserved.