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中,我们发现代码也可以正常的运行,好像问题不大;此时我们调用下接口报错,啪 快乐没了;

Abp vnext EFCore 实现动态上下文DbSet踩坑记

竟然报错!这是怎么回事呢,我们来看看报错的详细信息:

Abp vnext EFCore 实现动态上下文DbSet踩坑记

上图我们不难看出它说构造函数无法获取到参数 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
声明:原创博客请在转载时保留原文链接或者在文章开头加上本人博客地址,如发现错误,欢迎批评指正。凡是转载于本人的文章,不能设置打赏功能,如有特殊需求请与本人联系!

上一篇:八、Abp vNext 基础篇丨标签聚合功能


下一篇:redission快速入门