在我们应用程序的每个 SQL 表中,我们都有“CreatedDate”和“ModifiedDate”列。我们使用数据库优先的方法。当我保存数据时,我希望自动填充这两列。一种方法是在 SQL 表列本身上将默认值设置为
getdate()
。这样就部分解决了问题。这意味着当实体是新的时它将设置 CreatedDate 和 ModifiedDate。
但是,当我编辑/更新实体时,我只想更新 ModifiedDate
。请参阅此答案
.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();
}
...
对于那些使用异步系统(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 实体继承实体。
如果您想重写 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;
}
}
更简单,您不必重写所有方法。
您可以重写 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;}
}
从 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; }
}
我做了与 @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);
}
这是我在 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。
这是一种更通用的方法。
像这样配置实体: 不要忘记添加“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);
}
}
}
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());
}