Home > database >  EF Cannot be translated
EF Cannot be translated

Time:12-01

i have problem with translated query, ToList(), AsEnumerable etc.

I need construct or create query which is shared.

Branches -> Customer -> some collection -> some collection Customer -> some collection -> some collection.

Do you help me how is the best thingh how to do it and share the query.

i access to repository via graphql use projection etc.

public IQueryable<CustomerTableGraphQL> BranchTableReportTest(DateTime actualTime, long userId)
{
    var r =
    (
        from b in _dbContext.Branches
        let t = Customers(b.Id).ToList()
        select new CustomerTableGraphQL
        {
            Id = b.Id,
            Name = b.Name,
            Children =
            (
                from c in t
                select new CustomerTableGraphQL
                {
                    Id = c.Id,
                    Name = c.Name
                }
            )
            .AsEnumerable()
        }
    );

    return r;
}

public IQueryable<Customer> Customers(long branchId) => 
    _dbContext.Customers.Where(x => x.BranchId.Value == branchId).ToList().AsQueryable();

Some example how to doit and share iquearable between query

CodePudding user response:

Using ToList / AsEnumerable etc. entirely defeats the potential benefits of using IQueryable. If your code needs to do this rather than return an IQueryable<TEntity> then you should be returning IEnumerable<TResult> where TResult is whatever entity or DTO/ViewModel you want to return.

An example of an IQueryable<TEntity> repository pattern would be something like this:

public IQueryable<Customer> GetCustomersByBranch(long branchId) => 
    _dbContext.Customers.Where(x => x.BranchId.Value == branchId);

Normally I wouldn't really even have a repository method for that, I'd just use:

public IQueryable<Customer> GetCustomers() => 
    _dbContext.Customers.AsQueryable();

... as the "per branch" is simple enough for the consumer to request without adding methods for every possible filter criteria. The AsQueryable in this case is only needed because I want to ensure the result matches the IQueryable type casting. When your expression has a Where clause then this is automatically interpreted as being an IQueryable result.

So a caller calling the Repository's "GetCustomers()" method would look like:

// get customer details for our branch.
var customers = _Repository.GetCustomers()
    .Where(x => x.BranchId == branchId)
    .OrderBy(x => x.LastName)
    .ThenBy(x => x.FirstName)
    .Select(x => new CustomerSummaryViewModel
    {
         CustomerId = x.Id,
         FirstName = x.FirstName,
         LastName = x.LastName,
         // ...
    }).Skip(pageNumber * pageSize)
    .Take(pageSize)
    .ToList();

In this example the repository exposes a base query to fetch data, but without executing/materializing anything. The consumer of that call is then free to:

  • Filter the data by branch,
  • Sort the data,
  • Project the data down to a desired view model
  • Paginate the results

... before the query is actually run. This pulls just that page of data needed to populate the VM after filters and sorts as part of the query. That Repository method can serve many different calls without needing parameters, code, or dedicated methods to do all of that.

Repositories returning IQueryable that just expose DbSets aren't really that useful. The only purpose they might provide is making unit testing a bit easier as Mocking the repository is simpler than mocking a DbContext & DbSets. Where the Repository pattern does start to help is in enforcing standardized rules/filters on data. Examples like soft delete flags or multi-tenant systems where rows might belong to different clients so a user should only ever search/pull across one tenant's data. This also extends to details like authorization checks before data is returned. Some of this can be managed by things like global query filters but wherever there are common rules to enforce about what data is able to be retrieved, the Repository can serve as a boundary to ensure those rules are applied consistently. For example with a soft-delete check:

public IQueryable<Customer> GetCustomers(bool includeInactive = false)
{
    var query = _context.Customers.AsQueryable();
    if (!includeInactive)
        query = query.Where(x => x.IsActive);
    return query;
}

A repository can be given a dependency for locating the current logged in user and retrieving their roles, tenant information, etc. then use that to ensure that:

  • a user is logged in.
  • The only data retrieved is available to that user.
  • An appropriate exception is raised if specific data is requested that this user should never be able to access.

An IQueryable repository does require a Unit of Work scope pattern to work efficiently within an application. IQueryable queries do no execute until something like a ToList or Single, Any, Count, etc. are called. This means that the caller of the repository ultimately needs to be managing the scope of the DbContext that the repository is using, and this sometimes rubs developers the wrong way because they feel the Repository should be a layer of abstraction between the callers (Services, Controllers, etc.) and the data access "layer". (EF) To have that abstraction means adding a lot of complexity that ultimately has to conform to the rules of EF (or even more complexity to avoid that) or significantly hamper performance. In cases where there is a clear need or benefit to tightly standardizing a common API-like approach for a Repository that all systems will conform to, then an IQueryable pattern is not recommended over a general IEnumerable typed result. The benefit of IQueryable is flexibility and performance. Consumers decide and optimize for how the data coming from the Repository is consumed. This flexibility extends to cover both synchronous and asynchronous use cases.

CodePudding user response:

EF Core will translate only inlined query code. This query will work:

public IQueryable<CustomerTableGraphQL> BranchTableReportTest(DateTime actualTime, long userId)
{
    var r =
    (
        from b in _dbContext.Branches
        select new CustomerTableGraphQL
        {
            Id = b.Id,
            Name = b.Name,
            Children =
            (
                from c in _dbContext.Customers
                where c.BranchId == b.Id
                select new CustomerTableGraphQL
                {
                    Id = c.Id,
                    Name = c.Name
                }
            )
            .AsEnumerable()
        }
    );

    return r;
}

If you plan to reuse query parts, you have to deal with LINQKit and its ExpandableAttribute (will show sample on request)

  • Related