I'm trying to consolidate logic for accessing different tables using Entity Framework. I created an extension method for pulling all registrations from my registration entity where the person is attending:
public static IEnumerable<Registration> Attending(this IEnumerable<Registration> registrations)
{
return registrations.Where(r => r.Status == RegistrationStatus.Paid || r.Status == RegistrationStatus.Assigned || r.Status == RegistrationStatus.Completed);
}
This works great for queries like this:
var attendees = db.Registrations.Attending().ToList();
But it doesn't work when used in a subquery:
ProductTotals = db.Products.Where(p => p.EventID == ev.Id).Select(p => new ProductSummaryViewModel
{
ProductID = p.ProductID,
ProductName = p.Name,
Registrations = p.Registrations.Attending().Count(),
}).ToList();
I get the following error:
LINQ to Entities does not recognize the method
'System.Collections.Generic.IEnumerable1[Registration]
Attending(System.Collections.Generic.IEnumerable1[Registration])'
method, and this method cannot be translated into a store expression.
Is there any way re-use that code in a subquery?
The main thing you're trying to achieve is reusing the predicate that defines the meaning of Attending. You can do that by storing the expression in a readonly variable that is available to whoever needs it in your application, for example in a static class ExpressionConstants.
public static readonly Expression<Func<Registration, bool>> IsAttending =
r => r.Status == RegistrationStatus.Paid
|| r.Status == RegistrationStatus.Assigned
|| r.Status == RegistrationStatus.Completed;
Then you can do
var attendees = db.Registrations.Where(ExpressionConstants.IsAttending).ToList();
And used in the subquery:
ProductTotals = db.Products.Where(p => p.EventID == ev.Id).Select(p => new ProductSummaryViewModel
{
ProductID = p.ProductID,
ProductName = p.Name,
Registrations = p.Registrations.AsQueryable() // AsQueryable()
.Where(ExpressionConstants.IsAttending).Count(),
})
The AsQueryable() is necessary because p.Registrations probably is an ICollection.
Related
I have the following statement.
List<ApplicationUserDto> peers = _context.ApplicationUsers
.Select(m => new ApplicationUserDto
{
Id = m.Id,
MyCount = m.GroupMemberships
.Count(pg => pg.StudentGroup.ReviewRoundId == reviewRoundId)
}).ToList();
I have another class, called PeerStudentGroup derived from StudentGroup. In the Count() function, I do not want them to be included. I mean I want to count only if it is StudentGroup type (not another class derived from it). I wonder how I can achieve this. Any suggestions?
In this case you can use the is keyword which compares instance types. You should add !(pg.StudentGroup is PeerStudentGroup) to your condition.
Your code should look like this:
List<ApplicationUserDto> peers = _context.ApplicationUsers
.Select(m => new ApplicationUserDto
{
Id = m.Id,
MyCount = m.GroupMemberships
.Count(pg => pg.StudentGroup.ReviewRoundId == reviewRoundId && !(pg.StudentGroup is PeerStudentGroup))
}).ToList();
This might help to start with:
Apply a .Where (filtering) statement before the projection (Select) like
MyCount = m.GroupMemberships
.Where(gm => !(gm is PeerStudentGroup))
[alternatively] typeof(gm) != typeof(PeerStudentGroup)
.Count(pg => pg.StudentGroup.ReviewRoundId == reviewRoundId)
I need to check the same specific condition (where clause) many times:
return _ctx.Projects.Where(p => p.CompanyId == companyId &&
(p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId))).ToList()
The part after the '&&' will cause that the user isn't able to retrieve restricted projects.
I wanted to abstract this check to a function. In the future these conditions could change and I don't want to replace all the LINQ queries.
I did this with the following extension method:
public static IQueryable<Project> IsVisibleForResearcher(this IQueryable<Project> projects, string userId)
{
return projects.Where(p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId));
}
Now I can change the LINQ query to:
return _ctx.Projects.Where(p => p.CompanyId == companyId)
.IsVisibleForResearcher(userId).ToList()
This generates the same SQL query. Now my problem starts when I want to use this extension method on another DbSet that has projects.
Imagine that a company has projects. And I only want to retrieve the companies where the user can at least see one project.
return _ctx.Companies
.Where(c => c.Projects.Where(p =>
p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId))
.Any())
Here I also like to use the extension method.
return _ctx.Companies
.Where(c => c.Projects.AsQueryable().IsVisibleForCompanyAccount(userId).Any())
This throws following exception:
An exception of type 'System.NotSupportedException' occurred in
Remotion.Linq.dll but was not handled in user code
Additional information: Could not parse expression 'c.Projects.AsQueryable()': This overload of the method 'System.Linq.Queryable.AsQueryable' is currently not supported.
Than I created the following extension methods:
public static IEnumerable<Project> IsVisibleForResearcher(this ICollection<Project> projects, string userId)
{
return projects.Where(p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId));
}
But this didn't work also.
Does anyone has an idea?
Or a step in the right direction.
Btw I'm using Entity Framework Core on .NET Core
UPDATE:
Using a Expression<Func<>> resulted in the same exception:
'System.Linq.Queryable.AsQueryable' is currently not supported.
UPDATE 2
Thx #ivan-stoev for providing a solution.
I still have one problem. I also want to retrieve the count of 'visible' projects.
I fixed it by doing this:
var companies = _ctx.Companies
.WhereAny(c => c.Projects, Project.IsProjectVisibleForResearcher(userId))
.Select(c => new CompanyListDto
{
Id = c.Id,
Name = c.Name,
LogoId = c.LogoId,
ProjectCount = _ctx.Projects.Where(p => p.CompanyId == c.Id)
.Count(Project.IsProjectVisibleForResearcher(userId))
});
But I don't find a way to just use c.Projects instead of ctx.Projects.Where(p => p.CompanyId == c.Id)
The SQL that gets generated is correct, but I'd like to avoid this unneeded check.
Sincerely,
Brecht
Using expressions / custom methods inside the IQueryable<T> query expression has been always problematic and requires some expression tree post processing. For instance, LinqKit provides AsExpandable, Invoke and Expand custom extension methods for that purpose.
While not so general, here is a solution for your sample use cases w/o using 3rd party packages.
First, extract the expression part of the predicate in a method. The logical place IMO is the Project class:
public class Project
{
// ...
public static Expression<Func<Project, bool>> IsVisibleForResearcher(string userId)
{
return p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId);
}
}
Then, create a custom extension method like this:
public static class QueryableExtensions
{
public static IQueryable<T> WhereAny<T, E>(this IQueryable<T> source, Expression<Func<T, IEnumerable<E>>> elements, Expression<Func<E, bool>> predicate)
{
var body = Expression.Call(
typeof(Enumerable), "Any", new Type[] { typeof(E) },
elements.Body, predicate);
return source.Where(Expression.Lambda<Func<T, bool>>(body, elements.Parameters));
}
}
With this design, there is no need of your current extension method, because for Projects query you can use:
var projects = _ctx.Projects
.Where(p => p.CompanyId == companyId)
.Where(Project.IsVisibleForResearcher(userId));
and for Companies:
var companies = _ctx.Companies
.WhereAny(c => c.Projects, Project.IsVisibleForResearcher(userId));
Update: This solution is quite limited, so if you have different use cases (especially inside the Select expression as in your second update), you'd better resort to some 3rd party package. For instance, here is the LinqKit solution:
// LInqKit requires expressions to be put into variables
var projects = Linq.Expr((Company c) => c.Projects);
var projectFilter = Project.IsVisibleForResearcher(userId);
var companies = db.Companies.AsExpandable()
.Where(c => projects.Invoke(c).Any(p => projectFilter.Invoke(p)))
.Select(c => new CompanyListDto
{
Id = c.Id,
Name = c.Name,
LogoId = c.LogoId,
ProjectCount = projects.Invoke(c).Count(p => projectFilter.Invoke(p))
});
I'm trying to pass lambda expressions and a type to my DAL. I have this statement:
(entities).GetType().GetMethod("Where")
"entities" is the Table of entities on the DataContext.
When I run the statement I get a null even though Linq.Table inherits IQueryable.
Anyone have an idea?
Here is the entire method:
public object GetResultSet(Dictionary<Type, Func<object, bool>> values)
{
using (ICSDataContext db = DataContextFactory.CreateDataContext<ICSDataContext>(DataContexts.ICS))
{
foreach (var entry in values)
{
var property = db.GetType().GetProperty(entry.Key.Name + "s");
IQueryable entities = (IQueryable)property.GetValue(db, null);
var whereMethod = (entities).GetType().GetMethod("Where")
.MakeGenericMethod(Type.GetType(entry.Key.AssemblyQualifiedName));
return whereMethod.Invoke(entities, new object[] { entry.Value });
}
}
return null;
}
Thanks
As an alternative you could do something like
db.Set<Type>()
which will return you the DBSet of the appropriate type, with Where accessible without reflection. Also you may want to use Expression> rather than Func, expressions work on queryables where as funcs work on enumerables. If you pass a func into a Where clause it pulls the entire dbset down and processes it in memory.
Typed expressions are also a little easier to work with (intellesence, type checking).
Expression<Func<User,bool>> filter = c=>c.FirstName == "Bob";
As another alternative you can look into System.Linq.Dynamic, ScottGu has a write up on it here. The article and the code are old, but it works with EF 6. It allows things like
.Where("CategoryId=2 and UnitPrice>3")
From answer by LukeH under here:
var where1 = typeof(Queryable).GetMethods()
.Where(x => x.Name == "Where")
.Select(x => new { M = x, P = x.GetParameters() })
.Where(x => x.P.Length == 2
&& x.P[0].ParameterType.IsGenericType
&& x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
&& x.P[1].ParameterType.IsGenericType
&& x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))
.Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() })
.Where(x => x.A[0].IsGenericType
&& x.A[0].GetGenericTypeDefinition() == typeof(Func<,>))
.Select(x => new { x.M, A = x.A[0].GetGenericArguments() })
.Where(x => x.A[0].IsGenericParameter
&& x.A[1] == typeof(bool))
.Select(x => x.M)
.SingleOrDefault();
Then this:
var gmi = where1.MakeGenericMethod(typeof(T));
I wrote an extension method to get only approved absences out of a list of absences:
public static IQueryable<tblAbwesenheit> OnlyApprovedAbsences(this IQueryable<tblAbwesenheit> source)
{
return source.Where(a =>
(a.tblAbwesenheitsantraggenehmigungs.Any() && a.tblAbwesenheitsantraggenehmigungs.All(g => g.AbwesenheitsgenehmigungsstatusID == AbsenceStatusIds.Approved))
&& (!a.tblAbwesenheitsstornierunggenehmigungs.Any() || a.tblAbwesenheitsstornierunggenehmigungs.Any(g => g.AbwesenheitsgenehmigungsstatusID != AbsenceStatusIds.Approved)));
}
When I'm using this method with a "normal" Select, everything is fine:
context.tblAbwesenheits.OnlyApprovedAbsences().ToList()
However when I'm using it inside a Select statement, I get an error:
context.tblMitarbeiters.Select(m => new
{
Employee = m,
AbsencesForEmployee = m.tblAbwesenheits.OnlyApprovedAbsences()
})
.ToList();
LINQ to Entities does not recognize the method
'System.Linq.IQueryable1[Data.tblAbwesenheit]
OnlyApprovedAbsences(System.Linq.IQueryable1[Data.tblAbwesenheit])'
method, and this method cannot be translated into a store expression.
I have searched quite a lot, but could not find a way to teach Entity Framework to recognize my Method without expanding the query to
context.tblMitarbeiters.Select(m => new
{
Employee = m,
AbsencesForEmployee = m.tblAbwesenheits
.Where(a =>
(a.tblAbwesenheitsantraggenehmigungs.Any() && a.tblAbwesenheitsantraggenehmigungs.All(g => g.AbwesenheitsgenehmigungsstatusID == AbsenceStatusIds.Approved))
&& (!a.tblAbwesenheitsstornierunggenehmigungs.Any() || a.tblAbwesenheitsstornierunggenehmigungs.Any(g => g.AbwesenheitsgenehmigungsstatusID != AbsenceStatusIds.Approved)))
})
.ToList();
Is there a way to get EF to recognize my Method?
EF is trying to look for a SQL equivalent of your method and not finding one. It can find an equivalent of the expanded query, which is why that works.
You might be able to create an expression rather than a method
var OnlyApprovedAbsencesExpression = (a =>
(a.tblAbwesenheitsantraggenehmigungs.Any() && a.tblAbwesenheitsantraggenehmigungs.All(g => g.AbwesenheitsgenehmigungsstatusID == AbsenceStatusIds.Approved))
&& (!a.tblAbwesenheitsstornierunggenehmigungs.Any() || a.tblAbwesenheitsstornierunggenehmigungs.Any(g => g.AbwesenheitsgenehmigungsstatusID != AbsenceStatusIds.Approved)))
and then write something like
AbsencesForEmployee = m.tblAbwesenheits.Where(OnlyApprovedAbsencesExpression)
I have a large table and I need to select some fields not all of them.
I would to do something like this:
select column 2, column3 from tableName order by column2
But I'm trying it and it's doing a error.
public List<RiskCard> GetAllAcitveRiskCardsBasicProperties(Company company)
{
return GetDbSet<RiskCard>()
.Where(i => i.Company.CompanyId == company.CompanyId && i.Active == true)
.OrderBy(o => o.PremisesName)
.Select(o => new RiskCard { RiskCardId = o.RiskCardId, PremisesName = o.PremisesName}).ToList();
}
The error is;
The entity or complex type 'my.namespace.RiskCard' cannot be constructed in a LINQ to Entities query.
Try to follow the error message, don't construct an entity in your LINQ to Entities query, you can create an anonymous type instead. Then you can construct the entities in LINQ to object query later on :
var query = GetDbSet<RiskCard>()
.Where(i => i.Company.CompanyId == company.CompanyId && i.Active == true)
.OrderBy(o => o.PremisesName)
.Select(o => new { RiskCardId = o.RiskCardId, PremisesName = o.PremisesName})
.ToList();
return query.Select(o => new RiskCard { RiskCardId = o.RiskCardId, PremisesName = o.PremisesName})
.ToList();
Related question : The entity cannot be constructed in a LINQ to Entities query