EF Core 3.0 中所有表通用的基本实体

Base entity common to all tables in EF Core 3.0

我想使用所有实体通用的基础实体,因为每个 table 都应该有广告 ID、InsertDate 和 LastModifiedDate。

根据文档,我应该创建一个 BaseEntity 抽象 class 并且每个实体都应该从中继承。

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; }
}

一切正常,直到我开始添加关系。现在,在添加了与外键的关系之后,迁移仅创建了一个名为 BaseEntity 的大型 table 和鉴别器,但抽象基础实体应该只用于继承公共属性。

我读过 here 有 3 种类型的继承,但在 EF Core 3.0 中只有 TPH 可用。网上看例子有abstract base class 没有这个问题。

我想知道如果我在我的实现中遗漏了什么,请你们帮我找出来。

这个:

modelBuilder.Entity<BaseEntity>()

将 BaseEntity 声明为数据库实体。而是配置所有子类型。由于它们被映射到单独的表,因此需要单独的配置。 EG

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();
    }
}

这是使用基本实体的具体 class。

    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");
    }
}

最后,数据库上下文的这一添加处理了 date/time 的更新和 date/time 的更新。

    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
}