1

I am working with the ABP Framework using C# and Entity Framework Core, and I have the following query:

public async Task<ListResultDto<GetLinkedOrganizationsDto>> GetOrganizationLinkOptions(
string searchTerm,
OrganizationType organizationType)
{
        var searchTermLower = (searchTerm ?? string.Empty).ToLower();

        var currentOrganizationStateCode = (await _organizationRepository.GetQueryableAsync())
            .Where(o => o.TenantId == _currentTenant.Id)
            .Select(o => o.State)
            .FirstOrDefault();

        var currentStateId = (await _stateRepository.GetQueryableAsync())
            .Where(s => s.StateCode == currentOrganizationStateCode)
            .Select(s => s.Id)
            .FirstOrDefault();

        var organizations = (await _organizationRepository.WithDetailsAsync())
           .Where(o => o.OrganizationType == organizationType)
           .ToList();

        var tenants = await _tenantRepository.GetQueryableAsync();
        var organizationStates = (await _organizationStateRepository.GetQueryableAsync())
            .Where(os => os.StateId == currentStateId);

        var states = await _stateRepository.GetQueryableAsync();

        var linkOptions = (from organization in organizations
                           join tenant in tenants on organization.TenantId equals tenant.Id
                           join organizationState in organizationStates on organization.Id equals organizationState.OrganizationId
                           join state in states on organizationState.StateId equals state.Id
                           where tenant.Name.ToLower().Contains(searchTermLower)
                           select new GetLinkedOrganizationsDto
                           {
                               Id = tenant.Id,
                               Name = tenant.Name,
                               Address = organization.Address,
                               City = organization.City,
                               StateId = state.Id,
                               State = state.Name,
                               PostalCode = organization.PostalCode,
                               Country = organization.Country,
                               PhoneNumber = organization.PhoneNumber
                           })
                           .GroupBy(dto => dto.Id)
                           .Select(g => g.First())
                           .ToList();

        return new ListResultDto<GetLinkedOrganizationsDto>(linkOptions);
}

The query works as expected, but I have an issue with the following line of code:

var organizations = (await _organizationRepository.WithDetailsAsync())
           .Where(o => o.OrganizationType == organizationType)
           .ToList();

I'm materializing the query because if I don't do it, I'm getting the following error:

Cannot use multiple context instances within a single query execution.
Ensure the query uses a single context instance.

However, materializing the query at this point loads a lot of records into memory before applying the filters, which I want to avoid. I need the filtering to happen before the data is materialized.

Why am I encountering this error? How can I rewrite the query to avoid materializing the organizations prematurely while still applying the filters correctly?

Note: as you can see, the other queries like tenants and states do not need to be materialized before the query, and they are not throwing the exception like organizations does

2 Answers 2

3

Use navigation properties and do not use generic repositories, or repository-per-entity patterns, even when using IQueryable.

The root of your error will be that your repositories do not appear to be using the same scope instance of the DbContext. If each repository scopes its own DbContext, whether by using an injected Transient scope context, A DbContextFactory, or new-ing up a DbContext instance these queries cannot be linked together without materializing into in-memory collections. This is both wasteful for memory, and hits performance. Even then there are limitations to how effectively EF can merge IQueryables in place of just using navigation properties to build queries.

A common argument for Generic repositories is to follow SRP. (Single Responsibility Principle) The argument being that the repository should be the single go-to point for a particular entity. This is a misinterpretation of SRP. SRP states that code should serve a single actor, and by actor that is a single consumer. When you construct repositories per-entity and pass that repository to several consumers, you give that repository several competing reasons for change. Instead, if you want to introduce an IQueryable repository to make testing easier etc. then think of a repository like a Controller in MVC. If using a Controller / Service / Presenter, whatever, then the repository should serve that single actor, and any entities that actor needs. Generic repositories are also a pain as if you need 5 different entities you now have 5 separate dependencies just to get the data instead of one, or maybe two repositories. (Truly common stuff like lookups that are used identically might warrant a separate LookupRepository as an example)

At its simplest, DbSet is already a Repository, so whether using that or a repository to serve the class requesting the data, your query can be drastically simplified down to something like:

var query = _context.Organizations
    .Where(x => x.Tenant.Name.ToLower().Contains(searchTerm)
       && x.OrganizationType == organizationType
       && x.OrganizationState.StateId == currentStateId)
    .GroupBy(x => x.Tenant.Id)
    .First()
    .Select(x => new GetLinkedOrganizationsDto
    {
        Id = x.Tenant.Id,
        Name = x.Tenant.Name,
        Address = x.Address,
        City = x.City,
        StateId = x.OrganizationState.StateId, // really just currentStateId
        State = x.OrganizationState.State.Name,
        PostalCode = x.PostalCode,
        Country = x.Country,
        PhoneNumber = x.PhoneNumber
    });

var results = await query.ToListAsync();

With an IQueryable repository, replace _context with _repository.GetOrganizations(). Even if parameters for the query are optional, for instance if currentStateId is optional, this can be factored into the query building:

var query = _context.Organizations
    .Where(x => x.Tenant.Name.ToLower().Contains(searchTerm)
       && x.OrganizationType == organizationType);

if (currentStateId.HasValue)
    query = query.Where(x => x.OrganizationState.StateId == currentStateId);

var groupedQuery = query.GroupBy(x => x.Tenant.Id)
    .First()
    ...

It negates the need for lots of anaemic dependencies and avoids issues like resorting to premature materialization or other limitations/compromises you will continue to encounter with something like a Generic repository implementation.

Sign up to request clarification or add additional context in comments.

Comments

0

Understanding IQueryable

An object of a class that implements IEnumerable<...> represents a sequence of similar objects. You can ask for the first element, and when you have an element you can ask for the next element of the sequence, as long as there is one.

At its lowest level, this enumeration of elements is done using GetEnumerator and repeatedly calling MoveNext / Current, like this:

IEnumerable<T> myEnumerable = ...

// Get the enumerator and repeatedly ask for the next element of the sequence
using (Enumerator<T> myEnumerator = myEnumerable.GetEnumerator())
{
    while (myEnumerator.MoveNext())
    {
        // there is a next element in the sequence, get it and process it
        T element = myEnumerator.Current;
        Process(element);
    }
}

foreach and LINQ methods that don't return IEnumerable<...> like ToList, ToDictionary, FirstOrDefault, Sum, Any, etc, will deep down call GetEnumerator and MoveNext / Current.

Although an object of a class that implements IQueryable looks like an Enumerable sequence, it doesn't represetn the sequence itself. It represents the potential to create an enumerable sequence.

To be able to fetch the data to create the enumerable sequence, the IQueryable holds an Expression and a Provider.

The Expression represents what data must be fetched in some generic format. The Provider knows who holds this data (usually a Database management system) and what language is used to communicate with this DBMS (usually something like SQL).

As long as you use LINQ methods that return IQueryable<...>, only the Expression is changed. There is no communication with the database, no data is fetched. These methods are not costly (= don't need a lot of processing power). Concatenating these LINQ methods is fast. You don't need to use async-await for this.

On the other hand, when you use foreach, or one of the LINQ methods that don't return IQueryable<...>, like ToList, Any, Count, FirstOrDefault, then deep inside GetEnumerator and MoveNext / Current are called.

When GetEnumerator is called, the Expression is sent to the Provider, who will translate the Expression into a SQL query and send the query to the DBMS. The returned data is converted into an eneumerable sequence of which the Enumerator is returned.

Some IQueryable objects are smarter and wait with fetching the data until the first MoveNext, to prevent fetching data that is never accessed.

Even smarter IQueryable objects fetch the data "per page". Only the first few objects (100?) are fetched, so if you stop enumerating after the third element, not all 1000 elements are fetched. Only after you enumerate after the 100th element, the next page of 100 elements is fetched.

Back to your question

Now that you understand IQueryable, you might gather that it's useless to use async methods that return the IQueryable. After all: only the Expression and the Provider are created, there is no communication with the DBMS.

You'll also understand that you won't see async methods for LINQ methods that return IQueryable<...>, like WhereAsync. You will find methods like GetEnumeratorAsync, foreachasync, ToListAsync and FirstOrDefaultAsync, these methods really need to access the DBMS to create the return value.

var organizations = (await _organizationRepository.WithDetailsAsync())
       .Where(o => o.OrganizationType == organizationType)

Alas you didn't mention the return value of WithDetailsAsync. As stated before: if the database is not contacted, why make it an awaitable method? It seems that after the await the data is already fetched. The return value is represented as an IEnumerable<...>.

OR

You are using the async await in an a way that is not meant for IQueryable. If you really didn't mean to materialize data before your final ToList, then your methods should return IQueryable<...> not Task<IQueryable<...>> or something similar.

You use two repositories OrganisationRepository and TenantRepository. Do they use the same database connection (via DbContext), or two different databases?

If these repositories use different databases, then you do have to materialize the data before you can join them. In that case await GetQueryableAsync should return IEnumerable<...>, not IQueryable.

If they are from the same database, then the problem is that OrganisationRepository and TenantRepository each hold their own instance of a DbContext. And that is your problem.

The error says, that you are try to join object from different DbContext objects

When creation these Repositories, these classes should not create the DbContext, the creator of these classes should tell both Repositories which DbContext to use.

This is known as Dependency Injection. If you are not familiar with this concept, consider to read some background information about it.

The idea is, that you won't let the repository create the DbContext, but that you create the DbContext, and that you tell each repository object what DbContext object to use. Something like this:

using (MyDbContext dbContext = ...)
{
    var organisationRepository = new OrganisationRepository()
    {
        DbContext = dbContext,
    };
    var tenantRepository = new TenantRepository
    {
        DbContext = dbContext,
    };
    var stateRepository = new StateRepository
    {
        DbContext = dbContext,
    };

Now let the repositories only create IQueryable<...>, don't execute the queries yet!

Continuing within the using statement:

    IQueryable<Organization> organizations = _organizationRepository.WithDetails()
       .Where(organization => organization.OrganizationType == organizationType);

    IQueryable<Tenant> tenants = _tenantRepository.GetQueryable();
    IQueryable<OrganizationState> organizationStates =  _organizationStateRepository.GetQueryable())
        .Where(orgState => orgState.StateId == currentStateId);

    IQueryable<State>states = _stateRepository.GetQueryable();

    // LinkOptions is also an IQueryable:
    var linkOptions = (from organization in organizations
                       join tenant in tenants on organization.TenantId equals tenant.Id
                       join organizationState in organizationStates on organization.Id equals organizationState.OrganizationId
                       join state in states on organizationState.StateId equals state.Id
                       where tenant.Name.ToLower().Contains(searchTermLower)
                       select new GetLinkedOrganizationsDto
                       {
                           Id = tenant.Id,
                           Name = tenant.Name,
                           Address = organization.Address,
                           City = organization.City,
                           StateId = state.Id,
                           State = state.Name,
                           PostalCode = organization.PostalCode,
                           Country = organization.Country,
                           PhoneNumber = organization.PhoneNumber
                       })
                       .GroupBy(dto => dto.Id)
                       .Select(g => g.First());

Until now only IQueryables are created, because you never needed any data. The Joins, GroupBy and Select only changed the Expression in the one and only Provider you created..

Just before the return is the first moment you access the DbMs. Because you use ToList, GetEnumerator is called, which sends the Expression to the Provider. The Provider translates the Expression into SQL and executes the query. The fetched data is returned data as an enumerator.

This is the only place where the database is contacted. Use async-await here:

    var fetchedLinkOptions = await linkOptions.ToListAsync();
    return new ListResultDto<GetLinkedOrganizationsDto>(linkOptions);
}
// end of using statement, the DbContext is Disposed and cannot be used anymore

Conclusion

Use one DbContext
If you want to combine data from one database before you meterialize it, make sure that the classes use the same DbContext. Use Dependency Injection for this.

No async await for methods that return IQueryable
Postpone materializing the data as long as possible. Therefore no async methods are needed until the moment that you materialize: ToListAsync. Note that if your repository doesn't materialize data it won't need async methods anymore.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.