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

问题描述

我想使用所有实体通用的基本实体,因为每个表都应该有广告 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 的大表,带有鉴别器,但抽象基实体应该只用于继承公共属性

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

我想知道我的实现中是否遗漏了一些东西,请大家帮我找出来。

解决方法

这个:

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

这是一个使用基本实体的具体类。

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

最后,这个添加到数据库上下文处理更新日期/时间。

    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
}