Abp vnext EFCore 實現動態上下文DbSet踩坑記


背景

我們在用EFCore框架操作數據庫的時候,我們會遇到在 xxDbContext 中要寫大量的上下文 DbSet<>; 那我們表少還可以接受,表多的時候每張表都要寫一個DbSet, 大量的DbSet無異於是很蛋疼的一件事;而且看上去也很啰嗦,也不美觀;至此我們就開始了下邊的踩坑之旅;

EFCore 如何實現動態DbSet

我們網上百度一下千篇一律大概都是一下這種方式來實現動態的

  1. 我們一般都是先定義實體
public class UserJob: IEntity
{
    public Guid UserId { get; set; }

    public Guid UserId { get; set; }

    public string JobName { get; set; }

    public bool IsManager { get; set; }
}
  1. 在我們的 XXDbContext 中添加如下方法,在注釋我們之前寫的 DbSet<>
public class CoreDBContext : AbpDbContext<CoreDBContext>
{
    // public DbSet<UserJob> UserJob { get; set; }

    public CoreDBContext(DbContextOptions<CoreDBContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        DynamicDbSet(modelBuilder);

        base.OnModelCreating(modelBuilder);
    }

    /// <summary>
    /// 動態dbSet
    /// </summary>
    /// <param name="modelBuilder"></param>
    private static void DynamicDbSet(ModelBuilder modelBuilder)
    {
        foreach (var entityType in EntityType())
        {
            modelBuilder.Model.AddEntityType(entityType);
        }
    }

    /// <summary>
    /// 派生IEntity的實體
    /// </summary>
    /// <returns></returns>
    private static List<Type> EntityType()
    {
        return Assembly.GetExecutingAssembly().GetTypes()
            .Where(s => typeof(IEntity).IsAssignableFrom(s))
            .Select(s => s).ToList();
    }
}

至此我們發現EFCore中實現動態DbSet,最關鍵的一句就是 modelBuilder.Model.AddEntityType(entityType);就可以了;我們想要的方式也差不多完成了;但是Abp vnext中真的也是這樣子的嗎?我們往下看;

Abp vnext 中實現動態DbSet

  • 復制粘貼准備收工

按照上邊的方式我們代碼照搬到Abp vnext中,我們發現代碼也可以正常的運行,好像問題不大;此時我們調用下接口報錯,啪 快樂沒了;

image

竟然報錯!這是怎么回事呢,我們來看看報錯的詳細信息:

image

上圖我們不難看出它說構造函數無法獲取到參數 IRepository1[DotNet.EFCore.Entity.UserJob]`,那我們發現這個參數是構造函數從容器中獲取的,我們不難猜測到是不是沒有注入到容器中去?我們一想這玩樣兒也不是我們注冊進去的啊,這東西我哪兒會啊,此時Abp又背鍋了(心里已經罵了起來....);

但是又一想,Abp vnext集成EFCore時好像有個默認倉儲配置這東西;發現這東西好像在哪兒見過,果不其然我們發現有如下代碼

services.AddAbpDbContext<CoreDBContext>(options =>
{
    options.AddDefaultRepositories(includeAllEntities: true);
});

一頓F12,這也沒轍啊,怎么辦這不完犢子了嗎! 奔着不服輸的心那我們繼續看看源碼是怎么操作的:

  • 源碼解析看究竟哪一步出了問題

上邊我們也可以看到在 AddAbpDbContext<>中有添加默認倉儲,那我們就從這個 AddAbpDbContext<> 入手,看代碼我們發現了關鍵性的一句代碼 new EfCoreRepositoryRegistrar(options).AddRepositories();

public static IServiceCollection AddAbpDbContext<TDbContext>(this IServiceCollection services,
     Action<IAbpDbContextRegistrationOptionsBuilder> optionsBuilder = null)
     where TDbContext : AbpDbContext<TDbContext>
{
    var options = new AbpDbContextRegistrationOptions(typeof(TDbContext), services);

    optionsBuilder?.Invoke(options); // 初始化AbpCommonDbContextRegistrationOptions選項配置

    // 其他代碼...

    new EfCoreRepositoryRegistrar(options).AddRepositories(); // 添加倉儲

    return services;
}

跟着腳步我們繼續往里看,看 .AddRepositories()中到底做了什么, 發現了如下三個方法來注冊倉儲:

public virtual void AddRepositories()
{
    RegisterCustomRepositories(); // 注冊自定義倉儲
    RegisterDefaultRepositories(); // 注冊默認倉儲
    RegisterSpecifiedDefaultRepositories(); // 注冊指定的Entity倉儲
}

這里我們只看 RegisterDefaultRepositories() 這個方法,其他兩個我們先不看;

protected virtual void RegisterDefaultRepositories()
{
    if (!Options.RegisterDefaultRepositories) // 就是邊配置項的 options.AddDefaultRepositories(includeAllEntities: true);
    {
        return;
    }

    foreach (var entityType in GetEntityTypes(Options.OriginalDbContextType)) // 獲取所有的EntityType
    {
        if (!ShouldRegisterDefaultRepositoryFor(entityType))
        {
            continue;
        }

        RegisterDefaultRepository(entityType); // 注冊默認倉儲服務
    }
}

往循環這里看返現在遍歷EntityType,這里我似乎好像懂了寫什么,我們繼續看 GetEntityTypes(Options.OriginalDbContextType) 這個方法是一個抽象方法;

 protected abstract IEnumerable<Type> GetEntityTypes(Type dbContextType);

上邊的這個抽象方法的EFCore代碼實現EfCoreRepositoryCustomerRegistrar如下;

protected override IEnumerable<Type> GetEntityTypes(Type dbContextType)
{
    return DbContextHelper.GetEntityTypes(dbContextType);
}

internal static class DbContextHelper
{
    public static IEnumerable<Type> GetEntityTypes(Type dbContextType)
    {
        return
            from property in dbContextType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance)
            where
                ReflectionHelper.IsAssignableToGenericType(property.PropertyType, typeof(DbSet<>)) &&
                typeof(IEntity).IsAssignableFrom(property.PropertyType.GenericTypeArguments[0])
            select property.PropertyType.GenericTypeArguments[0];
    }
}

此時我們發現了這里讀取EntityType是從 xxDbContext里面反射讀取 DbSet<>屬性來拿所有的EntityType的;至此我們在返回來看那個循環,也就是說如果我們把xxDbContext中的DbSet<>屬性都刪除了GetEntityTypes必定是空的,Abp是不會往循環里走幫我們注入默認倉儲服務的;

至此我們已經發現了大概的問題出在哪兒,刪除了xxDbContext中的DbSet<>屬性,Abp無法獲取到EntityType, 不能實現默認倉儲注入

  • 發現問題解決問題

接合上邊的問題,我們解決的重點在於獲取到EntityType, 注冊默認倉儲實現;
話不多說直接開干,我們繼承一下EfCoreRepositoryCustomerRegistrar, 重寫下AddRepositories實現;

using System.Reflection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;

namespace DotNet.EFCore.EfCore;

public class EfCoreRepositoryCustomerRegistrar : EfCoreRepositoryRegistrar
{
    public EfCoreRepositoryCustomerRegistrar(AbpDbContextRegistrationOptions options) : base(options)
    {
    }

    public override void AddRepositories()
    {
        foreach (var entityType in GetEntityType())
        {
            RegisterDefaultRepository(entityType);
        }
    }

    private IEnumerable<Type> GetEntityType()
    {
        return Assembly.GetExecutingAssembly().GetTypes()
            .Where(s => typeof(IEntity).IsAssignableFrom(s)).ToList();
    }
}

添加一個IServiceCollection的擴展

using Volo.Abp.EntityFrameworkCore.DependencyInjection;

namespace DotNet.EFCore.EfCore;

public static class ServiceDynamicDbSet
{
    public static void AddDefaultRepositories(this IServiceCollection services)
    {
        // 傳遞一個AbpCommonDbContextRegistrationOptions類型,便於RepositoryRegistrarBase基類屬性注入
        var options = new AbpDbContextRegistrationOptions(typeof(CoreDBContext), services);

        // 我們上邊自定義獲取EntityType實現注入默認倉儲
        new EfCoreRepositoryCustomerRegistrar(options).AddRepositories();
    }
}

EntityFrameWorkCoreModule中添加如下代碼:

context.Services.AddDefaultRepositories();

至此我們運行代碼,發現好像貌似差不多可以了,到此大功告成;

小結

上述我們也不難發現,其實EFCore本身實現動態DbSet就是一行代碼的事兒,Abp vnext中不行是因為框架在注入默認倉儲的時候,通過獲取DbCotext中我們寫的DbSet來獲取實體類型,通過實體類型來注入倉儲默認實現的;

上述也是自己的踩坑經驗,也百度過也想白嫖(畢竟CV程序員嘛),但是都沒有對應的答案,所以才有此文,希望幫下其他伙伴給個參照;

可能也還有其他更優的解決方案,或者其中也存在bug,歡迎各位大佬指正;

以上代碼案例測試地址

作者:代碼驛站
本文地址:https://www.cnblogs.com/Jinfeng1213/p/15813900.html
聲明:原創博客請在轉載時保留原文鏈接或者在文章開頭加上本人博客地址,如發現錯誤,歡迎批評指正。凡是轉載於本人的文章,不能設置打賞功能,如有特殊需求請與本人聯系!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM