如何使用EF仓储(三)-CodeFirst

前言

本文主要介绍在Adnc框架中实体如何映射到数据库,示例采用CodeFirst模式,当然你也可以使用DBFirst模式。
本文所有操作都是在Adnc.Cust微服务中完成,其它微服务定义方式都一样。

如何映射

第一步,创建实体

我们需要在Adnc.Cus.Croe工程下的Entities目录创建实体,如Customer

如果采用经典三层模式开发,我们定义的实体必须直接继承或间接继承EfEntity类。

如果采用DDD架构模式开发, 我们定义的实体必须直接继承或间接继承AggregateRoot类。

Adnc.Cust 是采用经典三层模式开发的,DDD架构模式请参考Adnc.Ord/Adnc.Whse。实现模式都一样。

EfEntity 经典三层开发模式中所有实体类的基类

public abstract class EfBasicAuditEntity : EfEntity, IBasicAuditInfo

public abstract class EfFullAuditEntity : EfEntity, IFullAuditInfo

namespace Adnc.Cus.Core.Entities
{
    [Description("客户表")]
    public class Customer : EfFullAuditEntity
    {
        public string Account { get; set; }
  
        public string Nickname { get; set; }

        public string Realname { get; set; }

        public virtual CustomerFinance FinanceInfo { get; set; }

        public virtual ICollection<CustomerTransactionLog> TransactionLogs { get; set; }
    }
}

第二步,定义映射关系

Entities/Config目录下创建映射关系类,如CustomerConfig

namespace Adnc.Cus.Core.Entities.Config
{
    //EntityTypeConfiguration<TEntity>抽象类实现了IEntityTypeConfiguration<TEntity>接口
    public class CustomerConfig : EntityTypeConfiguration<Customer>
    {
        //覆写Configure基类方法
        public override void Configure(EntityTypeBuilder<Customer> builder)
        {
            //调用基类方法,映射公共接口字段
            base.Configure(builder);

            builder.HasOne(d => d.FinanceInfo).WithOne(p => p.Customer).HasForeignKey<CustomerFinance>(d => d.Id).OnDelete(DeleteBehavior.Cascade);

            builder.HasMany(d => d.TransactionLogs).WithOne().HasForeignKey(p => p.CustomerId).OnDelete(DeleteBehavior.Cascade);

            builder.Property(x => x.Account).IsRequired().HasMaxLength(16);

            builder.Property(x => x.Nickname).IsRequired().HasMaxLength(16);

            builder.Property(x => x.Realname).IsRequired().HasMaxLength(16);
        }
    }
}

很多示例中CustomerConfig是直接继承IEntityTypeConfiguration<TEntity>这个接口。我这里稍微封装了下。创建了一个EntityTypeConfiguration<TEntity>抽象类并实现了IEntityTypeConfiguration<TEntity>接口。然后我们实体关系映射类再继承这个抽象类。这样做主要是为了统一处理一些公共特性字段的映射。如软删除、并发列映射等等,代码如下。

namespace Adnc.Core.Shared.Entities.Config
{
    public abstract class EntityTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
       where TEntity : Entity
    {
        public virtual void Configure(EntityTypeBuilder<TEntity> builder)
        {
            var entityType = typeof(TEntity);
            //映射主键
            builder.HasKey(x => x.Idji);
            builder.Property(x => x.Id).ValueGeneratedNever();  
            //如果实体实现了IConcurrency接口,映射RowVersion字段
            if (typeof(IConcurrency).IsAssignableFrom(entityType))
            {
                builder.Property("RowVersion").IsRequired().IsRowVersion().ValueGeneratedOnAddOrUpdate();
            }
            //如果实体实现了ISoftDelete接口,映射IsDeleted字段
            if (typeof(ISoftDelete).IsAssignableFrom(entityType))
            {
                builder.Property("IsDeleted").HasDefaultValue(false);
                builder.HasQueryFilter(d => EF.Property<bool>(d, "IsDeleted") == false);
            }
        }
    }
}

第三步,创建EntityInfo类

Entities创建一个EntityInfo类,并实现IEntityInfo接口。这个类每个工程只需要定义一个,是公用的。我的项目模板生成工具写好后,项目模板生成工具生成的项目会包含这个类。GetEntitiesInfo()方法就是在当前程序集中查找继承了EfEntity的类,并放入集合中。

namespace Adnc.Cus.Core.Entities
{
    public class EntityInfo : AbstractEntityInfo
    {
        public override (Assembly Assembly, IEnumerable<Type> Types) GetEntitiesInfo()
        {
            var assembly = this.GetType().Assembly;
            var entityTypes = base.GetEntityTypes(assembly);

            return (assembly, entityTypes);
        }
    }
}

第四步,注入EntityInfo到容器

我们在工程根目录下的AdncCusCoreModule注册类里面注册。我的项目模板生成工具写好后,项目模板生成工具生成的项目会包含这个类并且同时会注册EntityInfo和其它组件。

namespace Adnc.Cus.Core
{
    public class AdncCusCoreModule : Module
    {
        /// <summary>
        /// Autofac注册
        /// </summary>
        /// <param name="builder"></param>
        protected override void Load(ContainerBuilder builder)
        {
            //注册其它组件
            //todo
            //注册EntityInfo
            builder.RegisterType<EntityInfo>().As<IEntityInfo>().InstancePerLifetimeScope();
            //注册其它组件
            //todo
        }
    }
}

第五步,生成迁移代码并更新到数据库

  • 设置Adnc.Cus.WebApi为启动项目(迁移命令会从这个工程读取数据库连接串)
  • 在VS工具中打开Nuget的程序包管理器控制台(工具=>Nuget包管理器=>程序包管理器控制台)
  • 设置“程序包管理器控制台”默认项目为Adnc.Cus.Migrations
  • 执行命令add-migration Update2021030401。执行成功后,会在Adnc.Cus.Migrations工程的Migrations目录下生成迁移文件。
  • 执行命令update-database,更新到数据库。

实体是如何与数据库关联起来的呢?

我们看Adnc.Infr.EfCore工程的AdncDbContext类的源码。

namespace Adnc.Infr.EfCore
{
    public class AdncDbContext : DbContext
    {
        private readonly UserContext _userContext;
        private readonly IEntityInfo _entityInfo;
        private readonly UnitOfWorkStatus _unitOfWorkStatus;

        public AdncDbContext([NotNull] DbContextOptions options
        , UserContext userContext
        //注入IEntityInfo
        , [NotNull] IEntityInfo entityInfo
        , UnitOfWorkStatus unitOfWorkStatus)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //获取实体信息
            var (Assembly, Types) = _entityInfo.GetEntitiesInfo();
            //这里
            foreach (var entityType in Types)
            {
                modelBuilder.Entity(entityType);
            }
            //这里
            modelBuilder.ApplyConfigurationsFromAssembly(Assembly);

            base.OnModelCreating(modelBuilder);
        }
    }
}

https://aspdotnetcore.net/ef-gurd/
https://aspdotnetcore.net/ef-unitofwork/
https://aspdotnetcore.net/ef-codefirst/
https://aspdotnetcore.net/ef-core-readwrite/