欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Abp vNext 自定义 Ef Core 仓储引发异常

程序员文章站 2022-05-03 12:48:32
问题 在使用自定义 Ef Core 仓储和 ABP vNext 注入的默认仓储时,通过两个 Repository 进行 Join 操作,提示 。这个异常信息翻译成中文的大概意思就是,你不能使用两个 DbContext 里面的 DbSet 进行 Join 查询。 如果将自定义仓储改为 进行注入,是可以 ......

问题

在使用自定义 ef core 仓储和 abp vnext 注入的默认仓储时,通过两个 repository 进行 join 操作,提示 cannot use multiple dbcontext instances within a single query execution. ensure the query uses a single context instance. 。这个异常信息翻译成中文的大概意思就是,你不能使用两个 dbcontext 里面的 dbset 进行 join 查询。

Abp vNext 自定义 Ef Core 仓储引发异常

Abp vNext 自定义 Ef Core 仓储引发异常

如果将自定义仓储改为 irepository<tentity,tkey> 进行注入,是可以与 _courserepostory 进行关联查询的。

我在 xxxentityframeworkcoremodule 的配置,以及自定义仓储 efcorestudentrepository 代码如下。

xxxentityframeworkcoremodule 代码:

public class xxxentityframeworkcoremodule : abpmodule
{
    public override void configureservices(serviceconfigurationcontext context)
    {
        context.services.addabpdbcontext<xxxdbcontext>(op =>
        {
            op.adddefaultrepositories();
        });
        
        configure<abpdbcontextoptions>(op => op.usepostgresql());
    }
}

efcorestudentrepository 代码:

public class efcorestudentrepository : efcorerepository<ixxxdbcontext, student, long>, istudentrepository
{
    public efcorestudentrepository(idbcontextprovider<ixxxdbcontext> dbcontextprovider) : base(dbcontextprovider)
    {
    }

    public task<int> getcountwithstudentlidasync(long studentid)
    {
        return dbset.countasync(x=>x.studentid == studentid);
    }
}

原因

原因在异常信息已经说得十分清楚了,这里我们需要了解两个问题。

  1. 什么原因导致两个仓储内部的 dbcontext 不一致?
  2. 为什么 abp vnext 自己实现的仓储能够进行关联查询呢?

首先我们得知道,仓储内部的 dbcontext是怎么获取的。我们的自定义仓储都会继承 efcorerepository ,而这个仓储是实现了 iquerable<t> 接口的,最终它会通过一个 idbcontextprovider<tdbcontext> 获得一个可用的 dbcontext

public class efcorerepository<tdbcontext, tentity> : repositorybase<tentity>, iefcorerepository<tentity>
    where tdbcontext : iefcoredbcontext
    where tentity : class, ientity
{
    public virtual dbset<tentity> dbset => dbcontext.set<tentity>();

    dbcontext iefcorerepository<tentity>.dbcontext => dbcontext.as<dbcontext>();

    // 这里可以看到,是通过 idbcontextprovider 来获得 dbcontext 的。
    protected virtual tdbcontext dbcontext => _dbcontextprovider.getdbcontext();

    protected virtual abpentityoptions<tentity> abpentityoptions => _entityoptionslazy.value;

    private readonly idbcontextprovider<tdbcontext> _dbcontextprovider;
    private readonly lazy<abpentityoptions<tentity>> _entityoptionslazy;

    // ... 其他代码。
}

下面就是 idbcontextprovider<tdbcontext> 内部的核心代码:

public class unitofworkdbcontextprovider<tdbcontext> : idbcontextprovider<tdbcontext> where tdbcontext : iefcoredbcontext
{
    private readonly iunitofworkmanager _unitofworkmanager;
    private readonly iconnectionstringresolver _connectionstringresolver;

    // ... 其他代码。

    public tdbcontext getdbcontext()
    {
        var unitofwork = _unitofworkmanager.current;
        if (unitofwork == null)
        {
            throw new abpexception("a dbcontext can only be created inside a unit of work!");
        }

        var connectionstringname = connectionstringnameattribute.getconnstringname<tdbcontext>();
        var connectionstring = _connectionstringresolver.resolve(connectionstringname);

        // 会构造一个 key,而这个 key 刚好是泛型类型的 fullname。
        var dbcontextkey = $"{typeof(tdbcontext).fullname}_{connectionstring}";

        // 内部是从一个字典当中,根据 dbcontextkey 获取 dbcontext。如果不存在的话则调用工厂方法创建一个新的 dbcontext。
        var databaseapi = unitofwork.getoradddatabaseapi(
            dbcontextkey,
            () => new efcoredatabaseapi<tdbcontext>(
                createdbcontext(unitofwork, connectionstringname, connectionstring)
            ));

        return ((efcoredatabaseapi<tdbcontext>)databaseapi).dbcontext;
    }

    // ... 其他代码。
}

通过以上代码我们就可以知道,abp vnext 在仓储的内部是通过 idbcontextprovider<tdbcontext> 中的 tdbcontext 泛型,来确定是否构建一个新的 dbcontext 对象。

不论是 abp vnext 针对 irepository<tentity,tkey> ,还是我们自己实现的自定义仓储,它们最终的实现都是基于 efcorerepository<tdbcontext,tentity,tkey> 的。而我们 idbcontextprovider<tdbcontext> 的泛型,也是这个仓储基类提供的,后者的 tdbcontext 就是前者的泛型参数。

所以当我们在模块添加 dbcontext 的过城中,只要调用了 adddefaultrepositories() 方法,abp vnext 就会遍历你提供的 tdbcontext 所定义的实体,然后为这些实体建立默认的仓储。

在注入仓储的时候,找到了获得默认仓储实现类型的方法,可以看到这里它使用的是 defaultrepositorydbcontexttype 作为默认的 tdbcontext 类型。

protected virtual type getdefaultrepositoryimplementationtype(type entitytype)
{
    var primarykeytype = entityhelper.findprimarykeytype(entitytype);

    // 重点在于构造仓储类型时,传递的 options.defaultrepositorydbcontexttype 参数,这个参数就是后面 efcorerepository 的 tdbcontext 泛型。
    if (primarykeytype == null)
    {
        return options.specifieddefaultrepositorytypes
            ? options.defaultrepositoryimplementationtypewithoutkey.makegenerictype(entitytype)
            : getrepositorytype(options.defaultrepositorydbcontexttype, entitytype);
    }

    return options.specifieddefaultrepositorytypes
        ? options.defaultrepositoryimplementationtype.makegenerictype(entitytype, primarykeytype)
        : getrepositorytype(options.defaultrepositorydbcontexttype, entitytype, primarykeytype);
}

最后我发现这个就是在模块调用 addabpcontext<tdbcontext> 所提供的泛型参数。

public abstract class abpcommondbcontextregistrationoptions : iabpcommondbcontextregistrationoptionsbuilder
{
    // ... 其他代码

    protected abpcommondbcontextregistrationoptions(type originaldbcontexttype, iservicecollection services)
    {
        originaldbcontexttype = originaldbcontexttype;
        services = services;
        defaultrepositorydbcontexttype = originaldbcontexttype;
        customrepositories = new dictionary<type, type>();
        replaceddbcontexttypes = new list<type>();
    }

    // ... 其他代码
}

public class abpdbcontextregistrationoptions : abpcommondbcontextregistrationoptions, iabpdbcontextregistrationoptionsbuilder
{
    public dictionary<type, object> abpentityoptions { get; }

    public abpdbcontextregistrationoptions(type originaldbcontexttype, iservicecollection services)
        : base(originaldbcontexttype, services) // 之类调用的就是上面的构造方法。
    {
        abpentityoptions = new dictionary<type, object>();
    }
}

public static class abpefcoreservicecollectionextensions
{
    public static iservicecollection addabpdbcontext<tdbcontext>(
        this iservicecollection services, 
        action<iabpdbcontextregistrationoptionsbuilder> optionsbuilder = null)
        where tdbcontext : abpdbcontext<tdbcontext>
    {
        // ... 其他代码。
        
        var options = new abpdbcontextregistrationoptions(typeof(tdbcontext), services);

        // ... 其他代码。

        return services;
    }
}

所以,我们的默认仓储的 dbcontextkeyxxxdbcontext,我们的自定义仓储继承 efcorerepository<ixxxdbcontext,tentity,tkey> ,所以它的 dbcontextkey 就是 ixxxdbcontext 。所以自定义仓储获取到的 dbcontext 就与自定义仓储的不一致了,从而提示上述异常。

解决

找到自定自定义仓储的定义,修改它 efcorereposiotry<tdbcontext,tentity,tkey>tdbcontext 泛型参数,变更为 xxxdbcontext 即可。

public class efcorestudentrepository : efcorerepository<xxxdbcontext, student, long>, istudentrepository
{
    public efcorestudentrepository(idbcontextprovider<xxxdbcontext> dbcontextprovider) : base(dbcontextprovider)
    {
    }

    public task<int> getcountwithstudentlidasync(long studentid)
    {
        return dbset.countasync(x=>x.studentid == studentid);
    }
}