这是另一种选择。
public abstract class BaseEntityWithUpdatedAndRowVersion
{
[Display(Name = "Updated By", Description = "User who last updated this meeting.")]
[Editable(false)]
[ScaffoldColumn(false)]
public string UpdatedBy { get; set; }
[Display(Name = "Updated", Description = "Date and time this row was last updated.")]
[Editable(false)]
[ScaffoldColumn(false)]
public DateTimeOffset UpdatedDateTime { get; set; }
[Display(Name = "SQL Server Timestamp", ShortName = "RowVersion", Description = "Internal SQL Server row version stamp.")]
[Timestamp]
[Editable(false)]
[ScaffoldColumn(false)]
public byte[] RowVersion { get; set; }
}
}
及其抽象配置:
internal abstract class BaseEntityWithUpdatedAndRowVersionConfiguration <TBase> : IEntityTypeConfiguration<TBase>
where TBase: BaseEntityWithUpdatedAndRowVersion
{
public virtual void Configure(EntityTypeBuilder<TBase> entity)
{
entity.Property(e => e.UpdatedBy)
.IsRequired()
.HasMaxLength(256);
entity.Property(e => e.UpdatedDateTime)
.HasColumnType("datetimeoffset(0)")
.HasDefaultValueSql("(sysdatetimeoffset())");
entity.Property(e => e.RowVersion)
.IsRequired()
.IsRowVersion();
}
}
这是一个使用基础实体的具体类。
public partial class Invitation: BaseEntityWithUpdatedAndRowVersion, IValidatableObject
{
[Display(Name = "Paper", Description = "Paper being invited.")]
[Required]
public int PaperId { get; set; }
[Display(Name = "Full Registration Fee Waived?", ShortName = "Fee Waived?",
Description = "Is the registration fee completely waived for this author?")]
[Required]
public bool IsRegistrationFeeFullyWaived { get; set; }
}
及其配置代码,调用基础配置:
internal class InvitationConfiguration : BaseEntityWithUpdatedAndRowVersionConfiguration<Invitation>
{
public override void Configure(EntityTypeBuilder<Invitation> entity)
{
base.Configure(entity);
entity.HasKey(e => e.PaperId);
entity.ToTable("Invitations", "Offerings");
entity.Property(e => e.PaperId).ValueGeneratedNever();
entity.HasOne(d => d.Paper)
.WithOne(p => p.Invitation)
.HasForeignKey<Invitation>(d => d.PaperId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("Invitations_FK_IsFor_Paper");
}
}
最后,对 db 上下文的添加处理更新的日期/时间和更新的日期/时间。
public partial class ConferenceDbContext : IdentityDbContext<ConferenceUser, ConferenceRole, int>
{
public override int SaveChanges()
{
AssignUpdatedByAndTime();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
AssignUpdatedByAndTime();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(cancellationToken).ConfigureAwait(true);
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(true);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "App is not being globalized.")]
private void AssignUpdatedByAndTime()
{
//Get changed entities (added or modified).
ChangeTracker.DetectChanges();
var changedEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
//Assign UpdatedDateTime and UpdatedBy properties if they exist.
AssignUpdatedByUserAndTime(changedEntities);
}
#region AssignUpdated
/// <summary>Assign updated-by & updated-when to any entity containing those attributes, including general category parent entities.</summary>
private void AssignUpdatedByUserAndTime(List<EntityEntry> changedEntities)
{
foreach (EntityEntry entityEntry in changedEntities)
{
//Some subcategory entities have the updated date/by attributes in the parent entity.
EntityEntry parentEntry = null;
string entityTypeName = entityEntry.Metadata.Name; //Full class name, e.g., ConferenceEF.Models.Meeting
entityTypeName = entityTypeName.Split('.').Last();
switch (entityTypeName)
{
case "Paper":
case "FlashPresentation":
case "Break":
parentEntry = entityEntry.Reference(nameof(SessionItem)).TargetEntry;
break;
default:
break;
}
AssignUpdatedByUserAndTime(parentEntry ?? entityEntry);
}
}
private void AssignUpdatedByUserAndTime(EntityEntry entityEntry)
{
if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
|| entityEntry.Entity is PaperRating)
{
PropertyEntry updatedDateTime = entityEntry.Property("UpdatedDateTime");
DateTimeOffset? currentValue = (DateTimeOffset?)updatedDateTime.CurrentValue;
//Avoid possible loops by only updating time when it has changed by at least 1 minute.
//Is this necessary?
if (!currentValue.HasValue || currentValue < DateTimeOffset.Now.AddMinutes(-1))
updatedDateTime.CurrentValue = DateTimeOffset.Now;
if (entityEntry.Properties.Any(p => p.Metadata.Name == "UpdatedBy"))
{
PropertyEntry updatedBy = entityEntry.Property("UpdatedBy");
string newValue = CurrentUserName; //ClaimsPrincipal.Current?.Identity?.Name;
if (newValue == null && !updatedBy.Metadata.IsColumnNullable())
newValue = string.Empty;
if (updatedBy.CurrentValue?.ToString() != newValue)
updatedBy.CurrentValue = newValue;
}
}
}
#endregion AssignUpdated
}