order by lambda expression with expression trees in C# - linq

My requirement is to order table according to a column in runtime. So I went through the path of parameter expression. I am able to achieve it when all the columns are present in single table. Here is my piece of code:
My requirement is to order table according to the buildStatusOrder during run time:
List<BuildStatusEnum> buildStatusOrder = new List<BuildStatusEnum>
{
BuildStatusEnum.h,
BuildStatusEnum.f,
BuildStatusEnum.a
};
ParameterExpression parameterExpression = Expression.Parameter(typeof(companyQueue));
MemberExpression memberExpression = Expression.PropertyOrField(parameterExpression, "buildStatusID");
MemberExpression valueExpression = Expression.Property(memberExpression, "Value");
Expression orderByExpression = Expression.Constant(buildStatusOrder.Count);
for (int statusIndex = buildStatusOrder.Count - 1; statusIndex >= 0; statusIndex--)
{
ConstantExpression constantExpression = Expression.Constant((int)buildStatusOrder[statusIndex]);
ConstantExpression indexConstantExpression = Expression.Constant(statusIndex);
BinaryExpression equalExpression = Expression.Equal(valueExpression, constantExpression);
orderByExpression = Expression.Condition(equalExpression, indexConstantExpression, orderByExpression);
}
MemberExpression hasValueExpression = Expression.Property(memberExpression, "HasValue");
ConditionalExpression nullCheckExpression = Expression.Condition(hasValueExpression, orderByExpression, Expression.Constant(buildStatusOrder.Count));
var orderByLambda = Expression.Lambda<Func<companyQueue, int>>(nullCheckExpression, parameterExpression);
using (KapowContext context = new KapowContext())
{
var queue = context.companyQueues
.Where(cq => cq.u.s.cq.userID == Utilities.Authentication.UserId)
.Where(cq => buildStatusOrder.Contains((BuildStatusEnum)cq.u.s.cq.buildStatusID))
.OrderBy(o => o.u.se.segmentId)
.ThenBy(orderByLambda)
This works fine. I have to display an additional column. It is from another table. So I joined tables. My lambda expression now looks like this:
var queue = context.companyQueues
.Join(context.abcd, cq => cq.robotID, r => r.robotId, (cq, r) => new { cq, r })
.Join(context.efgh, s => s.r.segmentId, se => se.segmentId, (s, se) => new { s, se })
.Join(context.hijk, u => u.s.cq.userID, us => us.userID, (u, us) => new { u, us })
.Where(cq => cq.u.s.cq.userID == Utilities.Authentication.UserId)
.Where(cq => buildStatusOrder.Contains((BuildStatusEnum)cq.u.s.cq.buildStatusID))
.OrderBy(orderByLambda)
Now my orderbyLambda is not working. I am getting the following error:
'System.Linq.Queryable.ThenBy<TSource,TKey>(System.Linq.IOrderedQueryable<TSource>, System.Linq.Expressions.Expression<System.Func<TSource,TKey>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Related

ExpressionTree creation for y => ConstantCollection.Any(x => y.Contains(x)). Different scope parameterExpressions

I'm trying to create an ExpressionTree from one constant string array and one parameter.
var keys = "car,train".Split(','); // "car, train" will be given as a constant.
//var myModel = "MyModel-car"; // this will be given as a parameter
myModel => keys.Any(k => myModel.Contains(k)); // how to create this ExpressionTree?
I've read through this, but my case is different because the delegate (k => myModel.Contains(k)) has actually two parameters. Here, the k is determined by keys which is a ConstantExpression at runtime. But in the Expression body, it is still a ParameterExpression instance.
I've tried like:
var modelParam = Expression.Parameter(typeof(string)); // Example value: "MyModel-car".
var keys = "car,train".Split(',').AsQueryable().Expression;
var anyMethod = typeof(Enumerable).GetMethods()
.First(x => x.Name == "Any" && x.GetParameters().Length == 1)
.MakeGenericMethod(typeof(string));
var containsMethod = typeof(String).GetMethods()
.Where(x => x.Name == "Contains" && x.GetParameters().Length == 1)
.First(x => x.GetParameters().First().ParameterType == typeof(string));
var key = Expression.Parameter(typeof(string));
var containsCallExpr = Expression.Call(modelParam, containsMethod, key);
var lambda = Expression.Lambda(containsCallExpr, key);
Expression anyCallExpr = Expression.Call(keys, anyMethod, lambda); // throw an exception
Expression expr = Expression.Lambda(anyCallExpr, modelParam);
However, the anycallExpr line throws an exception:
Static method requires null instance, non-static method requires non-null instance
Does anyone know how to make this work?
[UPDATE]
Based on #Svyatoslav Danyliv's suggestion, I changed the order of argument but a different exception is shown:
Incorrect number of arguments supplied for call to method 'Boolean Any[String](System.Collections.Generic.IEnumerable1[System.String])' (Parameter 'method')`
I think this exception makes sense because I have two parameters actually.
Try the following realization:
var modelParam = Expression.Parameter(typeof(string), "model") ; // Example value: "MyModel-car".
var keys = "car,train".Split(',').AsEnumerable();
var keysExpression = Expression.Constant(keys);
var containsMethod = typeof(string).GetMethods()
.Where(x => x.Name == nameof(string.Contains) && x.GetParameters().Length == 1)
.First(x => x.GetParameters().First().ParameterType == typeof(string));
var key = Expression.Parameter(typeof(string), "k");
var containsCallExpr = Expression.Call(modelParam, containsMethod, key);
var lambda = Expression.Lambda(containsCallExpr, key);
Expression anyCallExpr = Expression.Call(typeof(Enumerable), nameof(Enumerable.Any),
new[] { typeof(string) }, keysExpression, lambda);
Expression expr = Expression.Lambda(anyCallExpr, modelParam);

EF Core ToDictionary throw expression could not be translated

I have this query:
Project = await Context.Projects.Where(x => x.Id == project.Id)
.Select(x => new ProjectModel
{
Id = project.Id,
Name = project.Name,
TreeDataDict = x.Tasks.Select(y => new TreeItemModel()
{
NodeId = y.Id,
ParentId = SetTaskParentId(y, y.Type),
NodeTitle = y.Name,
Expanded = false,
Object = new TaskBaseModel
{
Milestone = y.Milestone,
StartDate = y.StartDate,
CurrentEndDate = y.CurrentEndDate,
EndDate = y.EndDate,
},
Icon = TaskHelper.GetTaskIcon(y.Type),
Children = new List<TreeItemModel>()
}).ToDictionary(y => y.NodeId, y => y)
}).SingleOrDefaultAsync();
and also tried like this:
Project = await Context.Projects.Where(x => x.Id == project.Id)
.Select(x => new ProjectModel
{
Id = project.Id,
Name = project.Name,
TreeDataDict = x.Tasks.ToDictionary(y => y.Id, y => new TreeItemModel(
{
NodeId = y.Id,
ParentId = SetTaskParentId(y, y.Type),
NodeTitle = y.Name,
Expanded = false,
Object = new TaskBaseModel
{
Milestone = y.Milestone,
StartDate = y.StartDate,
CurrentEndDate = y.CurrentEndDate,
EndDate = y.EndDate,
},
Icon = TaskHelper.GetTaskIcon(y.Type),
Children = new List<TreeItemModel>()
})
}).SingleOrDefaultAsync();
Both ways I got this exception:
What could be causing this? and is there a way I could make this work without fetching as a list and then covert it to dictionary? What could be the most efficient way to achieve this?
Regards
What could be causing this?
Translation of the nested ToDictionary call (none of the available overloads) is not supported. Consider it one of the (many) current (latest at the time of writing official v5.0.11) EF Core shortcomings.
Interestingly though, the Dictionary<TKey, TValue> constructor overload with IEnumerable<KeyValuePair<TKey, TValue>> argument as well as projection (Select) to KeyValuePair<TKey, TValue> is supported, which gives the workaround - replace the ToDictionary call with the aforementioned constructor and Select, e.g. (replace int with the type of the Id)
TreeDataDict = new Dictionary<int, TreeItemModel>(
x.Tasks.Select(y => new TreeItemModel()
{
NodeId = y.Id,
ParentId = SetTaskParentId(y, y.Type),
NodeTitle = y.Name,
Expanded = false,
Object = new TaskBaseModel
{
Milestone = y.Milestone,
StartDate = y.StartDate,
CurrentEndDate = y.CurrentEndDate,
EndDate = y.EndDate,
},
Icon = TaskHelper.GetTaskIcon(y.Type),
Children = new List<TreeItemModel>()
})
.Select(e => new KeyValuePair<int, TreeItemModel>(e.NodeId, e)));
This will fix the current error. But note that you are using other non-translatable constructs (custom method calls like SetTaskParentId(y, y.Type) and TaskHelper.GetTaskIcon(y.Type)) which are supported only in the final Select, so make sure to not add LINQ operator like Where, OrderBy etc. after the root query Select(x => new ProjectModel { ... }, otherwise you'll get other runtime errors. ToList, `First

how to modify or insert where into expression tree

by default I have this:
Expression<Func<ItemGroup, ItemGroupView>> Exp =
m => new ItemGroupView{
ID = m.id,
Name = m.name,
TotalCount = m.groupDetail.Sum(n => n.item.itemDetail.Count())
};
but in the runtime, I might want to add multiple filter. So for example, if I specify the status to 1 and category to mineral then it becomes
Expression<Func<ItemGroup, ItemGroupView>> Exp =
m => new ItemGroupView{
ID = m.id,
Name = m.name,
TotalCount = m.groupDetail.Sum(
n => n.item.itemDetail
.Where(o => o.status == 1 && o.category == "mineral")
.Count())
};
// ItemGroup.groupDetail is collection of ItemGroupDetail (n)
// ItemGroupDetail.item is Item
// Item.itemDetail is collection of ItemDetail (o)
// ItemDetail.item is Item
how do I modify the expression tree to insert multiple Where dynamically?
So far I do the default like this
private int _status;
private string _category;
internal Expression<Func<ItemDetail, bool>> whereStatus()
{
return o => o.status == _status;
}
internal Expression<Func<ItemDetail, bool>> whereCategory()
{
return o => o.category == _category ;
}
internal Expression<Func<ItemGroup, ItemGroupView>> GetEx()
{
return m => new ItemGroupView{
ID = m.id,
Name = m.name,
TotalCount = m.groupDetail.Sum(n => n.item.itemDetail.Count())
};
}
internal IQueryable<ItemGroupView> GetSelectQuery(IQueryabe<ItemGroup> ie)
{
ParameterExpression m = Expression.Parameter(typeof(ItemGroup), "m");
ParameterExpression n = Expression.Parameter(typeof(ItemGroupDetail), "n");
MemberInitExpression ex = (MemberInitExpression)GetEx().Body;
// ParameterReplacer is inherited from ExpressionVisitor
ex = (MemberInitExpression)new ParameterReplacer(
new ParameterExpression[] { m, n }).Visit(ex);
// ? ? ? ?
// how to modify the Expression if _status or _category is supplied?
Expression<Func<ItemGroup, ItemGroupView>> el =
Expression.Lambda<Func<ItemGroup, ItemGroupView>>
(ex, new ParameterExpression { m });
return ie.Select(el);
}
EDIT:
ItemGroup.itemDetail changed to ItemGroup.groupDetail, to avoid confusion between groups and items..
If you can create an expression that represents the Where() operation, you can then paste it into your main expression using LINQKit:
Expression<Func<IQueryable<ItemDetail>, IQueryable<ItemDetail>>> whereExpression=
id => id.Where(o => o.status == 1 && o.category == "mineral");
Expression<Func<ItemGroup, ItemGroupView>> Exp =
m => new ItemGroupView
{
ID = m.id,
Name = m.name,
TotalCount =
whereExpression.Invoke(m.itemDetail)
.Sum(n => n.item.itemDetail.Count())
};
Exp = Exp.Expand();
(Don't forget that last line, it's important.)

Dynamic Where clauses with multiple joins using Linq

I need to build a dynamic where clause in a Linq statement with multiple joins.
.Net 3.5
Linq-To-Sql
I have these incoming parameters for the Linq statement, only the "UID" is required.
int uid = 23702; // <-- Only one Required
string courseNumber = "";
string title = "";
int? categoryId = null;
int? typeId = null;
I've been testing this out in LinqPad and while I've gotten the query to work with all Where clauses in place, the Nullable int parameters end up returning incorrect results.
Here's my Linq statement:
var ci = course_instances;
var query = courses.Join(ci,
c => c.course_id,
i => i.course_id,
(c, i) => new
{
c = c,
i = i
}).Join(user_courses,
temp => temp.i.instance_id,
uc => uc.instance_id,
(temp, uc) => new
{
temp = temp,
uc = uc
})
.Where (temp1 => (temp1.uc.uid == uid))
.Where (temp1 => (temp1.temp.c.course_number.Contains(courseNumber)))
.Where (temp1 => (temp1.temp.c.title.Contains(title)))
//.Where (temp1 => (temp1.temp.c.course_type_id == typeId))
//.Where (temp1 => (temp1.temp.c.course_category_id == categoryId))
.Select (temp1 => new CourseSearchMyCourses
{
// snipped the many properties
});
I've tried using PredicateBuilder, but it returns the error:
The type arguments for method 'System.Linq.Queryable.Where(System.Linq.IQueryable, System.Linq.Expressions.Expression>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
Here's my PredicateBuilder Linq attempt:
var conditions = PredicateBuilder.True<user_course>();
conditions = conditions.And(c => c.uid == uid);
var ci = course_instances;
var query = courses.Join(ci,
c => c.course_id,
i => i.course_id,
(c, i) => new
{
c = c,
i = i
}).Join(user_courses,
temp => temp.i.instance_id,
uc => uc.instance_id,
(temp, uc) => new
{
temp = temp,
uc = uc
})
.Where (conditions)
.Select (temp1 => new CourseSearchMyCourses
{
// snipped the many properties
});
BTW, I also tried using "System.Linq.Dynamic" using string queries, and got the error the " and " isn't recognized.
Any help is appreciated.
Thanks.
Linq predicates with nullable type variables get translated into a SQL predicate = NULL. But that is totally different than what it should be: IS NULL.
You expect to get the rows where course_type_id is empty, but the = comparison returns no results because NULL is not a value and the comparison returns UNKNOWN. I think that is the cause of your "incorrect results".
If this is your problem, a fix can be found here.

LINQ: Group By + Where in clause

I'm trying to implement a T-SQL equivalent of a where in (select ...) code in LINQ.
This is what I have now:
int contactID = GetContactID();
IEnumerable<string> threadList = (from s in pdc.Messages
where s.ContactID == contactID
group 1 by new { s.ThreadID } into d
select new { ThreadID = d.Key.ThreadID}).ToList<string>();
var result = from s in pdc.Messages
where threadList.Contains(s.ThreadID)
group new { s } by new { s.ThreadID } into d
let maxMsgID = d.Where(x => x.s.ContactID != contactID).Max(x => x.s.MessageID)
select new {
LastMessage = d.Where(x => x.s.MessageID == maxMsgID).SingleOrDefault().s
};
However, my code won't compile due to this error for the ToList():
cannot convert from
'System.Linq.IQueryable<AnonymousType#1>'
to
'System.Collections.Generic.IEnumerable<string>'
Anyone have any suggestions on how to implement this? Or any suggestions on how to simplify this code?
Your query returns a set of anonymous types; you cannot implicitly convert it to a List<string>.
Instead, you should select the string itself. You don't need any anonymous types.
Change it to
var threadList = pdc.Messages.Where(s => s.ContactID == contactID)
.Select(s => s.ThreadID)
.Distinct()
.ToList();
var result = from s in pdc.Messages
where threadList.Contains(s.ThreadID)
group s by s.ThreadID into d
let maxMsgID = d.Where(x => x.ContactID != contactID).Max(x => x.MessageID)
select new {
LastMessage = d.Where(x => x.MessageID == maxMsgID).SingleOrDefault()
};

Resources