我想使用一个基础实体共同的所有实体,因为每个表应该有ad ID, InsertDate和LastModifiedDate。
根据文档,我应该创建一个BaseEntity抽象类,每个实体都应该继承它。
public abstract class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime? InsertDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? LastModifiedDateTime { get; set; }
}
一切都很好,直到我开始添加关系。现在,在添加了外键关系之后,Migration只创建了一个名为BaseEntity的带有Discriminators的大表,但是抽象的基本实体应该只用于继承公共属性。
我在这里读到有3种类型的继承,但在EF Core 3.0中只有TPH可用。在网络上看到的带有抽象基类的例子没有这个问题。
我想知道如果我在我的实现中缺少一些东西,如果你们帮我找出。
This:
modelBuilder.Entity<BaseEntity>()
声明BaseEntity为数据库实体。而是配置所有子类型。因为它们被映射到不同的表,所以它们需要单独的配置。如
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
if (et.ClrType.IsSubclassOf(typeof(BaseEntity)))
{
et.FindProperty("InsertDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("InsertDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd;
et.FindProperty("LastModifiedDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("LastModifiedDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
}
base.OnModelCreating(modelBuilder);
}
注意,这不会导致LastModifiedDateTime
在每次更改时都要更新。这就需要在EF中使用某种触发器或拦截器。
这是另一个选择。
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
}