.net如何優雅的使用EFCore

EFCore是微軟官方的一款ORM框架,主要是用於實體和數據庫對象之間的操作。功能非常強大,在老版本的時候叫做EF,後來.net core問世,EFCore也隨之問世。
本文我們將用一個控制檯項目Host一個web服務,並且使用本地Mysql作爲數據庫,使用EFCore的Code First模式進行數據操作。

DBSet清除計劃

以前使用EF/EFCore的開發者應該都記得,需要在DBContext裏寫好多DBSet,一個表對應一個DBSet,然後在其他地方操作這些DBSet對相關的表進行增刪改查。作爲一個開發,這些重複操作都是我們希望避免的,我們可以利用反射機制將這些類型通過框架自帶的方法循環註冊進去。
1.EF實體繼承統一的接口,方便我們反射獲取所有EF實體,接口可以設置一個泛型,來泛化我們的主鍵類型,因爲可能存在不同的表的主鍵類型也不一樣。
統一的EF實體接口

public interface IEFEntity<TKey>
{
    public TKey Id { get; set; }
}

統一的接口實現類

public abstract class AggregateRoot<TKey> : IEFEntity<TKey>
{
    public TKey Id { get; set; }
}

用戶實體類

public class User : AggregateRoot<string>
{
    public string UserName { get; set; }
    public DateTime Birthday { get; set; }
    public virtual ICollection<Book> Books { get; set; }
}

2.利用反射獲取某個程序集下所有的實體類

public class EFEntityInfo
{
    public (Assembly Assembly, IEnumerable<Type> Types) EFEntitiesInfo => (GetType().Assembly, GetEntityTypes(GetType().Assembly));
    private IEnumerable<Type> GetEntityTypes(Assembly assembly)
    {
        //獲取當前程序集下所有的實現了IEFEntity的實體類
        var efEntities = assembly.GetTypes().Where(m => m.FullName != null
                                                        && Array.Exists(m.GetInterfaces(), t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEFEntity<>))
                                                        && !m.IsAbstract && !m.IsInterface).ToArray();

        return efEntities;
    }
}

3.DBContext實現類中OnModelCreating方法中註冊這些類型

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //循環實體類型,並且通過Entity方法註冊類型
    foreach (var entityType in Types)
    {
        modelBuilder.Entity(entityType);
    }

    base.OnModelCreating(modelBuilder);
}

至此爲止所有的實體類都被註冊到DBContext中作爲DBSets,再也不需要一個個寫DBSet了,可以用過DbContext.Set<User>()獲取用戶的DBSet。

IEntityTypeConfiguration(表配置)

用數據庫創建過表的同學都知道,在設計表的時候,可以給表添加很多配置和約束,在Code First模式中,很多同學都是在對象中通過註解的方式配置字段。如下就配置了用戶名是不能爲NULL和最大長度爲500

[Required]
[MaxLength(500)]
public string UserName { get; set; }

也有的同學在DbContext中的OnModelCreating方法配置

modelBuilder.Entity<User>().Property(x => x.UserName).IsRequired();

這兩種方法,前者入侵行太強,直接代碼耦合到實體類中了,後者不夠清楚,把一大堆表的配置寫在一個方法裏,當然了很多人說可以拆分不同的方法或者使用註釋分開。但是!不夠優雅!
我們可以使用IEntityTypeConfiguration接口實現我們所想的優雅的表配置。
1.創建一個配置基類,繼承自IEntityTypeConfiguration,做一些通用的配置,比如設置主鍵,一般都是id啦,還有軟刪除等。

public abstract class EntityTypeConfiguration<TEntity, TKey> : IEntityTypeConfiguration<TEntity>
       where TEntity : AggregateRoot<TKey>
{
    public virtual void Configure(EntityTypeBuilder<TEntity> builder)
    {
        var entityType = typeof(TEntity);

        builder.HasKey(x => x.Id);

        if (typeof(ISoftDelete).IsAssignableFrom(entityType))
        {
            builder.HasQueryFilter(d => EF.Property<bool>(d, "IsDeleted") == false);
        }
    }
}

2.創建用戶實體/表獨有的配置,比如設置用戶名的最大長度,以及seed一些數據

public class UserConfig : EntityTypeConfiguration<User, string>
{
    public override void Configure(EntityTypeBuilder<User> builder)
    {
        base.Configure(builder);

        builder.Property(x => x.UserName).HasMaxLength(50);
        //mock一條數據
        builder.HasData(new User()
        {
            Id = "090213204",
            UserName = "Bruce",
            Birthday = DateTime.Parse("1996-08-24")
        });
    }
}

當然還有很多配置可以設置,比如索引,導航屬性,唯一鍵等。如下圖書實體

public class BookConfig : EntityTypeConfiguration<Book, long>
{
    public override void Configure(EntityTypeBuilder<Book> builder)
    {
        base.Configure(builder);

        builder.Property(x => x.Id).ValueGeneratedOnAdd(); //設置book的id自增
        builder.Property(x => x.BookName).HasMaxLength(500).IsRequired();
        builder.HasIndex(x => x.Author);//作者添加索引
        builder.HasIndex(x => x.SN).IsUnique();//序列號添加唯一索引
        builder.HasOne(r => r.User).WithMany(x=>x.Books)
            .HasForeignKey(r => r.UserId).IsRequired(false);//導航屬性,本質就是創建外鍵,雖然查詢很方便,生產中不建議使用!!!
    }
}

3.DBContext中應用配置

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasCharSet("utf8mb4 ");
    var (Assembly, Types) = _efEntitysInfo.EFEntitiesInfo;
    foreach (var entityType in Types)
    {
        modelBuilder.Entity(entityType);
    }
    //只需要將配置類所在的程序集給到,它會自動加載
    modelBuilder.ApplyConfigurationsFromAssembly(Assembly);
    base.OnModelCreating(modelBuilder);
}

Repository(倉儲)

這個不過分介紹,特別是基於http的微服務中基本都有這個。
1.創建一個倉儲基類,對於不同的實體,創建一樣的增刪改查方法。
簡單寫幾個查詢的方法定義。

public interface IAsyncRepository<TEntity, Tkey> where TEntity : class
{
    IQueryable<TEntity> All();
    IQueryable<TEntity> All(string[] propertiesToInclude);
    IQueryable<TEntity> Where(Expression<Func<TEntity, bool>> filter);
    IQueryable<TEntity> Where(Expression<Func<TEntity, bool>> filter, string[] propertiesToInclude);
}

2.創建倉儲實現類,將DBContext注入到構造中

public class GenericRepository<TEntity, Tkey> : IAsyncRepository<TEntity, Tkey> where TEntity : class
{
    protected readonly LibraryDbContext _dbContext;

    public GenericRepository(LibraryDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    ~GenericRepository()
    {
        _dbContext?.Dispose();
    }

    public virtual IQueryable<TEntity> All()
    {
        return All(null);
    }
    public virtual IQueryable<TEntity> All(string[] propertiesToInclude)
    {
        var query = _dbContext.Set<TEntity>().AsNoTracking();

        if (propertiesToInclude != null)
        {
            foreach (var property in propertiesToInclude.Where(p => !string.IsNullOrWhiteSpace(p)))
            {
                query = query.Include(property);
            }
        }

        return query;
    }
}

Autofac

1.注入DBContext到Repository的構造方法中,並且注入Repository

public class EFCoreEleganceUseEFCoreModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.RegisterModule<EFCoreEleganceUseDomainModule>(); //注入domain模塊
        builder.RegisterGeneric(typeof(GenericRepository<,>))//將dbcontext注入到倉儲的構造中
                .UsingConstructor(typeof(LibraryDbContext))
                .AsImplementedInterfaces()
                .InstancePerDependency();

        builder.RegisterType<WorkUnit>().As<IWorkUnit>().InstancePerDependency();
    }
}

2.Domain注入EFEntityInfo

public class EFCoreEleganceUseDomainModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<EFEntityInfo>().SingleInstance();
    }
}

數據庫配置

1.注入DBContext,從配置文件讀取數據庫配置,然後根據開發/生產環境做一些特殊處理

var mysqlConfig = hostContext.Configuration.GetSection("Mysql").Get<MysqlOptions>();
var serverVersion = new MariaDbServerVersion(new Version(mysqlConfig.Version));
services.AddDbContext<LibraryDbContext>(options =>
{
    options.UseMySql(mysqlConfig.ConnectionString, serverVersion, optionsBuilder =>
    {
        optionsBuilder.MinBatchSize(4);
        optionsBuilder.CommandTimeout(10);
        optionsBuilder.MigrationsAssembly(mysqlConfig.MigrationAssembly);//遷移文件所在的程序集
        optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
    }).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

    //開發環境可以打開日誌記錄和顯示詳細的錯誤
    if (hostContext.HostingEnvironment.IsDevelopment())
    {
        options.EnableSensitiveDataLogging();
        options.EnableDetailedErrors();
    }
});

項目架構和源碼

項目只是一個demo架構,並不適用於生產,主程序是一個控制檯項目,只需要引用相關的包和模塊,就可以啓動一個web host.

全部代碼已經全部上傳到github:https://github.com/BruceQiu1996/EFCoreDemo
該項目是一個可以啓動運行的基於.net6的控制檯項目,啓動後會啓動一個web host和一個swagger頁面。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章