Evaluate EF query with MAX on the server - entity-framework-core

Using Entity Framework Core 2.2 I have the following query:
var user = await context.Users.AsNoTracking()
.Include(x => x.Lessons).ThenInclude(x => x.LessonLevel)
.FirstOrDefaultAsync(x => x.Id == userId);
var lessons = context.Lessons.AsNoTracking();
.Where(x => x.LessonLevelId < user.Lessons.Max(y => y.LessonLevelId));
Thus query evaluates locally and I get the message:
The LINQ expression 'Max()' could not be translated and will be evaluated locally.'
How can I make this query evaluate on the server?
Update
Based on DavigG answer I made it work using:
var maxLessonLevelId = user.Lessons.Max(y => y.LessonLevelId););
var lessons = context.Lessons.AsNoTracking();
.Where(x => x.LessonLevelId < maxLessonLevelId);
I know the following evaluates locally but shouldn't evaluate on the server?
var lessons = context.Lessons.AsNoTracking();
.Where(x => x.LessonLevelId <
context.Users.AsNoTracking()
.Where(y => y.Id == userId)
.Select(y => y.Lessons.Max(z => z.LessonLevelId))
.FirstOrDefault());
Is it possible to use a child queries that evaluates on the server?

Get the max value as a separate query, for example:
var maxLessonLevelId = user.Lessons.Max(y => y.LessonLevelId);
Then you can can get the lessons like this:
var lessons = context.Lessons.AsNoTracking()
.Where(x => x.LessonLevelId < maxLessonLevelId);

Related

EF.Functions.Like() for an array of string

I want to filter the IQueryable<T> with the help of EF.Functions.Like() method, which accepts a string parameter, to make use of array of strings. Also, I want this filter to be applied on an IQueryable and not on a List.
var configurations = _dbContext.Configurations
.Include(x => x.ChildTable)
.Where(x => x.Id == Id);
if (!string.IsNullOrEmpty(request.Filter))
{
configurations = configurations.Where(x => EF.Functions.Like(string.Join(", ", x.ChildTable.Select(x => x.Name).ToArray()), '%' + request.Filter + '%'));
}
In the above code, Configuration has a one-many relationship with the ChildTable. Meaning, each configuration will have a more than one ChildTable entries related. So, with that given, x.ChildTable.Select(x => x.Name) is of type IEnumerable<string>. And, I want to filter the Configuration records whose ChildTable.Name entries is like the given request.Filter.
Try the following query:
var configurations = _dbContext.Configurations
.Include(x => x.ChildTable)
.Where(x => x.Id == Id);
if (!string.IsNullOrEmpty(request.Filter))
{
configurations = configurations
.Where(x => x.ChildTable.Any(c => EF.Functions.Like(c.Name, '%' + request.Filter + '%')));
}

EF Core rewrite the query in a form that can be translated after upgrading

This query was working fine before we upgraded:
var appUsers = await _masterDbContext
.Users
.Include(x => x.UserCustomers)
.AsNoTracking()
.Select(AppUserDto.Projection)
.Where(user => !user.IsDeleted && user.UserCustomers.Any(x => x.CustomerId == tenant.Id && x.UserId == user.Id))
.DistinctBy(x => x.Id)
.ToDataSourceResultAsync(request.GridOptions, cancellationToken: cancellationToken);
We're now getting the error:
Either rewrite the query in a form that can be translated,
or switch to client evaluation explicitly by inserting a call to 'AsEnumerable',
'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
It appears that the DistinctBy is the offender but being fairly new to LINQ I can't figure out how to rewrite it so that it works.
Change DistinctBy to Distinct() and move that and the predicate before the Select. I also shifted the AsNoTracking() up:
var appUsers = await _masterDbContext
.Users
.AsNoTracking()
.Include(x => x.UserCustomers)
.Where(user =>
!user.IsDeleted
&& user.UserCustomers
.Any( x => x.CustomerId == tenant.Id ) )
.Distinct()
.Select(AppUserDto.Projection)
.ToDataSourceResultAsync(request.GridOptions, cancellationToken: cancellationToken);
Looks you have upgraded from EF Core 2.x, which loads full filtered set into the memory. DistinctBy is not translatable by EF Core 6.
Try the following solution:
var filtered = _masterDbContext.Users
.Select(AppUserDto.Projection)
.Where(user => !user.IsDeleted && user.UserCustomers.Any(x => x.CustomerId == tenant.Id && x.UserId == user.Id));
var query =
from d in filtered.Select(d => new { d.Id }).Distinct()
from u in filtered.Where(u => u.Id == d.Id).Take(1)
select u;
var appUsers = await query.ToDataSourceResultAsync(request.GridOptions, cancellationToken: cancellationToken);

Linq query using result of another query

I have a query that gets data in the form of an IQueryable
var assys = assetrelationshipRepository.GetAll()
.Where(x => x.AssetId == siteAssetId)
.Where(x => x.RelationshipTypeId == (long)AssetRelationshipTypeEnum.Parent)
.Where(x => x.RelatedAsset.AssetTypeId == (long)AssetTypeEnum.Assembly)
.Select(x => x.RelatedAsset.CustomAssetAttributes2);
For every 'assy' that is returned, I'd like to get it's AssetId and use this to get a list of 'subassys', see below. For each 'assy' record, the assyId variable should be substituted for its AssetId.
var subassys = assetrelationshipRepository.GetAll()
.Where(x => x.AssetId == assyId)
.Where(x => x.RelationshipTypeId == (long)AssetRelationshipTypeEnum.Parent)
.Where(x => x.RelatedAsset.AssetTypeId == (long)AssetTypeEnum.SubAssy)
.Select(x => x.RelatedAsset.CustomAssetAttributes2);
I assume I'll need to use ForEach, does anyone know if what I'm trying to do is possible?
Thanks
Applying ForEach would be extremely bad approach to retrieve your desired result. You could apply join and group for that queries instead.
var assysQuery = assetrelationshipRepository.GetAll()
.Where(x => x.AssetId == siteAssetId)
.Where(x => x.RelationshipTypeId == (long)AssetRelationshipTypeEnum.Parent)
.Where(x => x.RelatedAsset.AssetTypeId == (long)AssetTypeEnum.Assembly);
Then apply join and group;
var subAssysQuery =
from assy in assysQuery
join subAssy in assetrelationshipRepository.GetAll() on assy.Id equals subAssy.AssetId
where
subAssy.RelationshipTypeId == (long)AssetRelationshipTypeEnum.Parent &&
subAssy.RelatedAsset.AssetTypeId == (long)AssetTypeEnum.Assembly
group assy by assy.Id into g
select new
{
AssyId = g.Key,
SubAssets = g.Select(x => x.RelatedAsset.CustomAssetAttributes2),
};
This should give you CustomAssetAttributes2 value of subassys for per assy record.
Note : Also, I suggest you to use known type for select clause instead
of anonymous type.

Linq to Entities using Lambda Expressions and multiple where conditions

I am trying to select a list of objects at the end of a fairly long chain of joins/selects using Linq to Entities written as Lambda Expressions... Here is what I have currently the following two statements.
var formDefId = _unitOfWork.AsQueryableFor<FormTrack>()
.Where(x => x.FormTrackId == formTrackId)
.Select(x => x.FormDefId).First();
var rules = _unitOfWork.AsQueryableFor<FormTrack>()
.Where(x => x.FormTrackId == formTrackId)
.Select(x => x.FormDef)
.SelectMany(x => x.Events
.Where(y => y.EventTypeId == 7))
.Select(x => x.RuleGroup)
.SelectMany(x => x.Rules)
.SelectMany(x => x.RuleFormXmls
.Where(y => y.FormDefId == formDefId));
What I would like to do, is combine the two queries, and use the FormDefId returned by
.Select(x => x.FormDef)
in the final where clause instead of having to use the formDefId from a separate query.
Is this something that is possible?
Thank you in advance for your help
It is much easier to write this using query syntax.. Each from in query syntax corresponds to a SelectMany in lambda syntax. This allows you to have all the variables in scope.
var rules =
from ft in _unitOfWork.AsQueryableFor<FormTrack>()
from e in ft.FormDef.Events
from r in e.RuleGroup.Rules
from x in r.RuleFormXmls
where ft.FormTrackId == formTrackId
where e.EventTypeId == 7
where x.FormDefId == ft.FormDefId
select x

How to include related field in query

I have an Entity Called Attachment.
Id/Name/ParentId
I have a second entity called AttachmentVersion.
Id/AttachmentId/Size/Date/other unwanted fields
It is related by an Id.
This is the query I want to write.
_context.Attachments.Where(a=> a.ParentId == 5)
//Include AttachementVersion.Size only in result.
If you want to get only most recent AttachmentVersion, then you should query AttachmentVersions:
using System.Data.Entity;
var result = db.AttachmentVersions
.Where(m => m.AttachmentId == attachmentId)
.Include(m => m.Attachment)
.OrderByDescending(m => m.Date)
.FirstOrDefault();
Update
If you want to get list of attachments according to some filter, you can achieve what you want by querying AttachmentVersions and grouping them.
var result = db.AttachmentVersions
.Where(m => m.Attachment.ParentId == 5)
.Include(m => m.Attachment)
.GroupBy(m => m.AttachmentId)
.Select(g => new
{
// There will be one Attachment because we
// grouped by AttachmentId
Attachments = g.Select(m => m.Attachment).Take(1),
// Get only size of latest version
Sizes = g.OrderByDescending(m => m.Date)
.Select(m => m.Size)
.Take(1)
})
// Load into memory to be able to use Single()
.AsEnumerable()
.Select(m => new
{
Attachment = m.Attachemts.Single(),
LastSize = m.Sizes.Single()
})
.ToList();