问题

在使用自定义 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 查询。

如果将自定义仓储改为 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);
    }
}