Entity Framework - reusing associated extension methods - c#

I'm trying to reuse an EF6 extension method with an associated entity (one to many relationship). Contrived example:
public class Parent
{
public string State { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public string Value { get; set; }
public Parent Parent { get; set; }
}
public static ParentNamedScopes
{
public static IQueryable<Parent> IsReady(this IQueryable<Parent> queryable)
{
return queryable.Where(p => p.State == "Ready" || p.State == "New");
}
}
// ...
var children = db.Children
// my goal, but can't cast Parent to IQueryable<Parent>
// ------------------v
.Where(c => c.Parent.IsReady())
.Where(c => c.Value == "Foobar");
I've seen examples of using AsQueryable() on associated collections in sub queries, but that isn't an options since Parent is a single record. I'm sure I'm missing something obvious and I apologize since my google foo has not turned up the answer today.

One option would be to start your query with the Parents:
var children = db.Parents.IsReady()
.SelectMany(p => p.Children)
.Where(c => c.Value == "Foobar");
The idea that IsReady would convert an IQueryable seems a little off to me, though. If your use case gets more complex, you may need to change that to just give you an Expression<Func<Parent, bool>>, and use something like LINQKit to manipulate your query to make it reusable:
Expression<Func<Parent, bool>> parentIsReady = ParentCriteria.IsReady();
var readyParents = db.Parents.Where(parentIsReady);
var childrenWithReadyParents = db.Children.AsExpandable()
.Where(c => parentIsReady.Invoke(c.Parent))
.Where(c => c.Value == "Foobar");

Related

EF Include(Expression)

I am trying to build generic method which would look simething like this:
public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(this IQueryable<TModel> query, Func<TModel, IQueryable<TIncludable>> includes, List<int> userIDs)
where TModel : class
where TIncludable: class
{
Expression<Func<TModel, object>> result = x => includes(x);
if(typeof(ASqlBase).IsAssignableFrom(typeof(TIncludable)))
{
result = x =>
includes(x)
.Select(prop => prop as ASqlBase)
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
.Select(prop => prop as TIncludable);
}
query = query.Include(result);
return query;
}
This method would allow me to centrally check if user can read navigation property's value and, if so, include it in result. My applications read rights are conceived in hierarchical way: logged user can read his records and records of all users he had added to the system. Because of that, I cannot determine all visible records in compile-time and, thus, cannot use different database contexts for different groups of users. Also, since this is only one of many ways for filtering data, unfortunately I cannot make use of Global Filters.
I am trying to call the above method like this:
qry = qry.IncludeByUserCondition<AllocatedFund, AllocatedFundDetailPaymentMade>(p => p.AllocatedFundDetailPaymentsMade.AsQueryable(), allowedUserIDs);
However, when I try to invoke it in run-time, I get the following exception:
The expression 'Invoke(__includes_0, x).Select(prop => (prop As ASqlBase)).Where(prop => ((prop.DeleteDate == null))).Where(prop => True).Select(prop => (prop As AllocatedFundDetailPaymentMade))' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
On another hand, when I try to run query manually, everything works fine:
qry = qry.Include(p =>
p.AllocatedFundDetailPaymentsMade
.AsQueryable()
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
Since I have more than just one navigation property to include in this manner (and I have to perform query in similar manner for all 30+ other models I use in my application), I wouldn't want to manually write those where clauses in every query.
Does anybody know the solution for this problem? Any help would be kindly appreciated.
EDIT:
ASqlBase is just base, abstract class from which some other models interit (although not all of them - ie. User model does not inherit from ASqlBase).
ASqlBase looks like this:
public abstract class ASqlBase
{
[Key]
public int ID { get; set; }
[Required]
public int UserID { get; set; }
[ForeignKey("UserID")]
public virtual User User { get; set; }
public DateTime? DeleteDate { get; set; }
}
I plan to use that function to get data and then display it in report. I'm giving example for Person, then the method call would look something like this:
var qry = dbContext.Person.IncludeByUserCondition<Person, Athlete>(p => p.Athletes.AsQueryable(), athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Employee>(p => p.Employees.AsQueryable(), employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Student>(p => p.Students.AsQueryable(), studentAllowedUserIDs);
Person model looks something like this:
public class Person : ASqlBase
{
...
public virtual ICollection<Athlete> Athletes { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
All of the above models: Athlete, Employee and Student inherit from ASqlBase
EDIT 2:
Sorry for bad method naming, method should be called IncludeByUserCondition, not IncludeMultiple (as it was named before).
A little bit simplified usage. You do not need to call AsQueryable() and explicitly specify generic parameters:
var qry = dbContext.Person.IncludeByUserCondition(p => p.Athletes, athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Employees, employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Students, studentAllowedUserIDs);
And realization:
public static class IncludeExtensions
{
public static IQueryable<TModel> IncludeByUserCondition<TModel, TRelated>(this IQueryable<TModel> query,
Expression<Func<TModel, IEnumerable<TRelated>>> collectionProp, List<int> userIDs)
where TModel : class
where TRelated : ASqlBase
{
var relatedParam = Expression.Parameter(typeof(TRelated), "r");
// r.DeleteDate == null
var filterPredicate = (Expression)Expression.Equal(
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.DeleteDate)),
Expression.Constant(null, typeof(DateTime?)));
if (userIDs?.Count > 0)
{
// r.DeleteDate == null && userIDs.Contains(r.UserID)
filterPredicate = Expression.AndAlso(filterPredicate,
Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(int) },
Expression.Constant(userIDs),
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.UserID))));
}
// r => r.DeleteDate == null && userIDs.Contains(r.UserID)
var filterLambda = Expression.Lambda(filterPredicate, relatedParam);
// p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID))
var transformedProp = Expression.Lambda(Expression.Call(typeof(Enumerable), nameof(Enumerable.Where),
new[] { typeof(TRelated) }, collectionProp.Body, filterLambda), collectionProp.Parameters);
// query.Include(p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID)))
var includeExpression = Expression.Call(typeof(EntityFrameworkQueryableExtensions),
nameof(EntityFrameworkQueryableExtensions.Include),
new[] { typeof(TModel), typeof(IEnumerable<TRelated>) },
query.Expression,
Expression.Quote(transformedProp));
// instantiate new IQueryable<TModel>
var resultQuery = query.Provider.CreateQuery<TModel>(includeExpression);
return resultQuery;
}
}

Failing to get a List<> of objects after a LINQ query

I am trying to convert my the result of my LINQ query to a list, but without a success until now.
This is my action method :
public ActionResult DisplayListOfRolesUser()
{
string currentUserId = User.Identity.GetUserId();//get the id of the logged user
UserDetails userDetails = db.UsersDetails.Where(c => c.identtyUserId == currentUserId)
.FirstOrDefault();
int UsrCompanyId = userDetails.CompanyId;//get the user's company
List<WorkRole> WorkRolesQuery = db.WorkRoles.Where(c => c.CompanyId == UsrCompanyId)
.FirstOrDefault().ToList();//get all the work roles for the compnay.
//List<WorkRole> lst = WorkRolesQuery.ToList();
return View(lst);
}
I tried many answers from stack overflow, but without success.
Currently List<WorkRole> WorkRolesQuery = db.WorkRoles.Where(c => c.CompanyId == UsrCompanyId).FirstOrDefault().ToList();
ToList is underlined with red and it says 'WorkRole' does not contain a definition for 'ToList'.
I simply want to have a list of WorkRole objects so I can display them in my view in some form.
Can somebody help?
Here is my model as well :
public class WorkRole
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int WorkRoleId { get; set; }
public string RoleName { get; set; }
public string RoleDescription { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
public virtual ICollection<WorkRolesUsersDetails> WorkRolesUsersDetails { get; set; }
}
When you call .FirstOrDefault().ToList();, this means that you first select the first item (if existing, null otherwise) and then try to convert this one item to a list (which fails).
Simply calling ToList() after the Where() statement should do it:
List<WorkRole> WorkRolesQuery = db.WorkRoles.Where(c => c.CompanyId == UsrCompanyId).ToList();
FirstOrDefault returns just the very first instance of type WorkRole whose CompanyId is equal to your UsrCompanyId, not an entire list. So you canĀ“t call ToList on that instance. I suppose you can omit the call to FirstOrDefault:
var WorkRolesQuery = db.WorkRoles
.Where(c => c.CompanyId == UsrCompanyId)
.ToList();
You try to get first element (only one element) calling FirstOrDefault() and cast it to list.
Replace this line:
List<WorkRole> WorkRolesQuery =
db.WorkRoles
.Where(c => c.CompanyId == UsrCompanyId)
.FirstOrDefault()
.ToList();
by this:
List<WorkRole> WorkRolesQuery =
db.WorkRoles
.Where(c => c.CompanyId == UsrCompanyId)
.ToList(); // without FirstOrDefault()
What you're looking for is ...
List<WorkRole> WorkRolesQuery = db.WorkRoles.Where(c => c.CompanyId == UsrCompanyId).ToList();
As FirstOrDefault returns a single entity.
You can't use ToList() because FirstOrDefault() returns one item or null. If you are expecting more than one item to be returned, use While() and then ToList(), don't use FirstOrDefault().
var WorkRoles = db.WorkRoles.Where(c => c.CompanyId == UsrCompanyId).ToList();
Also note if you are using FirstOrDefault(), you can simply pass in the condition without having to use Where().
var WorkRole = db.WorkRoles.FirstOrDefault(c => c.CompanyId == UsrCompanyId);
You can do :
List<WorkRole> WorkRolesQuery = new List<WorkRole>(db.WorkRoles
.Where(c => c.CompanyId == UsrCompanyId));

Sorting nested collection in projection: Unable to cast object of type 'SortOp' to type 'ProjectOp'

I'm using projection of query results to a custom type, which isn't a part of entity data model:
public sealed class AlgoVersionCacheItem : NotificationObject
{
public int OrderId { get; set; }
public string OrderTitle { get; set; }
public int? CurrentVersion { get; set; }
public int CachedVersion { get; set; }
public IEnumerable<int> AvailableVersions { get; set; }
}
I want AvailableVersions to be sorted in descending order. Hence, I've tried to add sorting for AvailableVersions in projection:
return someQueryable
.Select(version => new AlgoVersionCacheItem
{
OrderId = version.OrderId,
OrderTitle = version.Order.Title,
CurrentVersion = version.Order.CurrentAlgoVersionId,
CachedVersion = version.Id,
AvailableVersions = version
.Order
.AlgoVersions
.Where(v => (allowUncommittedVersions || v.Statuses.Any(s => s.AlgoVersionStatusListItemId == ModelConstants.AlgoVersionCommitted_StatusId)) && v.Id != version.Id)
.OrderByDescending(v => v.Id) // this line will cause exception
.Select(v => v.Id)
})
.Where(item => item.AvailableVersions.Any())
.OrderByDescending(item => item.OrderId)
.ToArray();
With sorting, execution of the query throws an System.Data.EntityCommandCompilationException with System.InvalidCastException as inner exception:
Unable to cast object of type
'System.Data.Entity.Core.Query.InternalTrees.SortOp' to type
'System.Data.Entity.Core.Query.InternalTrees.ProjectOp'
Without .OrderByDescending(v => v.Id) everything works fine.
Is this yet another feature, that isn't supported in Entity Framework, or I've missed something?
P.S. I know, that I can sort items later at client side, but I'm wondering about sorting at the server side.
This is a bug in EF. I was able to repro this on both EF5 and EF6. I think you should be able to workaround the bug by filtering records before creating the results i.e.:
return someQueryable
.Where(version => version.Order.AlgoVersions.Any(v => (allowUncommittedVersions || v.Statuses.Any(s => s.AlgoVersionStatusListItemId == ModelConstants.AlgoVersionCommitted_StatusId)) && v.Id != version.Id))
.Select(version => new AlgoVersionCacheItem
{
OrderId = version.OrderId,
OrderTitle = version.Order.Title,
CurrentVersion = version.Order.CurrentAlgoVersionId,
CachedVersion = version.Id,
AvailableVersions = version
.Order
.AlgoVersions
.Where(v => (allowUncommittedVersions || v.Statuses.Any(s => s.AlgoVersionStatusListItemId == ModelConstants.AlgoVersionCommitted_StatusId)) && v.Id != version.Id)
.OrderByDescending(v => v.Id) // this line will cause exception
.Select(v => v.Id)
})
.OrderByDescending(item => item.OrderId)
.ToArray();
I also have a feeling that this query could be simplified if you go from the other side of relationship (i.e. from Orders) but it may depend on how the someQueryable is created.
In my case the Linq query was selecting a new anonymous construct with one property being set to a collection ToList() extension method. I removed the embedded execution, the error went away, and the system still worked fine.

GROUP BY and HAVING clauses in nHibernate QueryOver

I'm trying to write this specific sql query in nHibernate QueryOver language, which I am not very familiar with:
SELECT MessageThreadId FROM MessageThreadAccesses
WHERE ProfileId IN (arr)
GROUP BY MessageThreadId
HAVING COUNT(MessageThreadId) = arr.Count
where arr is a array of integers(user Ids) I'm passing as argument and MessageThreadAccess entity looks like this:
public virtual MessageThread MessageThread { get; set; }
public virtual Profile Profile { get; set; }
....
After reading multiple stack overflow threads and experimenting I got this far with my query (trying to get MessageThread object - it should always be just one or none), but it still doesn't work and I'm not really sure what else to try. The query always seems to be returning the MessageThreadAccess object, but when reading it's MessageThread property it's always NULL.
var access = Session.QueryOver<MessageThreadAccess>()
.WhereRestrictionOn(x => x.Profile).IsIn(participants.ToArray())
.Select(Projections.ProjectionList()
.Add(Projections.Group<MessageThreadAccess>(x => x.MessageThread))
)
.Where(
Restrictions.Eq(Projections.Count<MessageThreadAccess>(x => x.MessageThread.Id), participants.Count)
)
.TransformUsing(Transformers.AliasToBean<MessageThreadAccess>())
.SingleOrDefault();
return Session.QueryOver<MessageThread>()
.Where(x => x.Id == access.MessageThread.Id)
.SingleOrDefault();
Can someone point me in the right direction, or explain what am I doing wrong?
Thanks in advance.
I guess you may try using a DTO for storing the result, instead of trying to fit the result in a MessageThreadAccess, when it is not one (no Profile).
Maybe you can try :
public class MessageThreadCountDTO
{
public MessageThread Thread { get; set; }
public int Nb { get; set; }
}
then
var profiles = new int[] { 1,2,3,4 };
MessageThreadCountDTO mtcDto = null;
var myResult =
_laSession.QueryOver<MessageThreadAccess>()
.WhereRestrictionOn(x => x.Profile.Id).IsIn(profiles)
.SelectList(list =>
list.SelectGroup(x => x.MessageThread).WithAlias(() => mtcDto.Thread).
SelectCount(x => x.MessageThread).WithAlias(() => mtcDto.Nb)
)
.Where(Restrictions.Eq(Projections.Count<MessageThreadAccess>(x => x.MessageThread), profiles.Count()))
.TransformUsing(Transformers.AliasToBean<MessageThreadCountDTO>())
.List<MessageThreadCountDTO>().FirstOrDefault();
would profiles be a Profile[], and not an int[], then the following line :
.WhereRestrictionOn(x => x.Profile.Id).IsIn(profiles)
should be :
.WhereRestrictionOn(x => x.Profile).IsIn(profiles)
Hope this will help

Exception "Specified method is not supported." being thrown from NHibernate IQueryable expression when using .Any extension

Utilizing NHibernate I am attempting to use a lambda expression to retrieve objects based on the status and values between a parent child relationship. AbstractWorkflowRequestInformation has a collection of WorkflowRequestInformationAction. Each of the two classes have their own Status properties. To illustrate here are the abbreviated classes as they relate to this query:
public class AbstractWorkflowRequestInformation
{
public virtual RequestStatus RequestStatus { get; set; }
public virtual IEnumerable<WorkflowRequestInformationAction>
WorkflowRequestInformationActionList { get; set; }
}
public class WorkflowRequestInformationAction
{
public virtual ActionStatus Status { get; set; }
public virtual string RoleIdentifier { get; set; }
public virtual string RoleName { get; set; }
}
Given this relationship I want to retrieve AbstractWorkflowRequestInformation objects based on a List<KeyValuePair<string, string>> called roles. I realize that the exception is being caused by a lack of parsing of the Any(...) extension method, but I am unsure of alternative queries. Thus far all permutations on the below query have caused the same or similar exceptions:
public IEnumerable<IRequestInformation> GetRequestsPendingActionBy(
List<KeyValuePair<string, string>> roles)
{
var results = GetSession().Query<AbstractWorkflowRequestInformation>()
.Where(r => r.RequestStatus == RequestStatus.Pending
&& r.WorkflowRequestInformationActionList
.Any(a => ActionStatus.Pending == a.Status
&& roles.Any(kp => kp.Key == a.RoleName
&& kp.Value == a.RoleIdentifier)))
.ToList();
return results;
}
The ultimate goal is to retrieve only those AbstractWorkflowRequestInformation objects which are pending and have a pending WorkflowRequestInformationAction matching a KeyValuePair in the roles enumerable.
I am not wedded to using a lambda expression as this expression has already grown unwieldy, if there's a more elegant ICriteria expression I am all ears. What are my options to restrict my results based upon the values in my roles List<KeyValuePair<string, string>> but prevent the "Specified method is not supported" exception?
I think this would get same results...
WorkflowRequestInformationAction actionAlias = null;
var q = GetSession().QueryOver<AbstractWorkflowRequestInformation>()
.Inner.JoinAlias(x => x.WorkflowRequestInformationActionList,
() => actionAlias)
.Where(x => x.RequestStatus == RequestStatus.Pending)
.And(() => actionAlias.Status == ActionStatus.Pending);
var d = Restrictions.Disjunction();
foreach(var kvp in roles)
{
d.Add(Restrictions.Where(() => actionAlias.RoleName == kvp.Key
&& actionAlias.RoleIdentitifier == kvp.Value));
}
q.And(d).TransformUsing(Transformers.DistinctRootEntity);
var results = q.List();
You could probably take a similar approach with NH Linq. I'm more comfortable with QueryOver/Criteria though.
The LINQ provider in NHibernate is not fully supported, you are trying to execute an extension method on a part of the expression tree that is not parsed from the provider.
This post might help you solve the problem. Be sure to checkout the related posts.
Also see the post from Fabio Maulo on NHibernate LINQ provider extension.

Categories