In our project we have texts for multiple languages stored in our database.
I want to create a helper function that includes a text in a query.
This would be useful because this include happens a lot in the application and I want have the include code in one place.
The include should use the new filtered includes from Entity Framework Core 5.
This is what I want to replace:
.Include(c => c.NameTexts.Where(t => t.LanguageId == langId))
Replace it for:
.IncludeText(e => e.NameTexts, langId)
The function I want to write:
// The helper function:
public static IQueryable<T> IncludeText<T>(this IQueryable<T> originalQuery, Expression<Func<T, IEnumerable<UserText>>> textToInclude, int langId) where T : class
{
var textWhere = textToInclude.Where(e => e.LanguageId == langId);
originalQuery.Include(textWhere);
return originalQuery;
}
// And call it from a query like this:
var result = await _context.SomeEntity
.IncludeText(e => e.NameTexts, langId)
// Instead of
// .Include(c => c.NameTexts.Where(t => t.LanguageId == langId))
.Where(c => c.Id == request.Id)
.SingleOrDefaultAsync(cancellationToken);
I tried doing the following but I get an error because the types don't match.
Expression<Func<UserText, bool>> newPred = t => t.LanguageId == langId;
var textWhere = Expression.Lambda<Func<T, IList<UserText>>>(Expression.AndAlso(textToInclude, newPred), textToInclude.Parameters);
originalQuery.Include(textWhere);
return originalQuery;
Maybe this can work :
public static IQueryable<T> IncludeText<T>(this IQueryable<T> originalQuery, Expression<Func<T, IEnumerable<UserText>>> textToInclude, int langId) where T : class
{
var methodWhere = typeof(Enumerable)
.GetMethods()
.First(m => m.ToString() == "System.Collections.Generic.IEnumerable`1[TSource] Where[TSource](System.Collections.Generic.IEnumerable`1[TSource], System.Func`2[TSource,System.Boolean])")
.MakeGenericMethod(typeof(UserText));
Expression<Func<UserText, bool>> predicate = t => t.LanguageId == langId;
var navigationWhere = Expression.Call(methodWhere, textToInclude.Body, predicate);
var lambda = Expression.Lambda<Func<T, IEnumerable<UserText>>>(navigationWhere, Expression.Parameter(typeof(T)));
return originalQuery.Include(lambda);
}
I dislike how Where MethodInfo is recuperated. You can improve by inspiring from this other question.
Related
I have an entity called Product and inside it a navigation property called ProductAttributes.
I need to get products with multiple conditions like this:
var products = context.Products
.Include(x => x.ProductAttributes)
.ThenInclude(x => x.Attribute)
.AsNoTracking()
.Where(x => x.ProductAttributes.FirstOrDefault(x => x.AttributeId == attributesIds[0]) != null
&& x.ProductAttributes.FirstOrDefault(x => x.AttributeId == attributesIds[1]) != null
&& x.ProductAttributes.FirstOrDefault(x => x.AttributeId == attributesIds[2]) != null);
I've tried to build the expression dynamically, but all of my tries failed.
This is the code I've written:
var arg = Expression.Parameter(typeof(Product), "x");
var left = Expression.Property(arg, "ProductAttributes");
var right = Expression.Constant(null);
var exp = Expression.NotEqual(left, right);
Expression<Func<Product, bool>> expression = Expression.Lambda<Func<Product, bool>>(exp, arg);
foreach (var id in attributesIds)
{
var searchArg = Expression.Parameter(typeof(ProductAttribute), "z");
var searchLeft = Expression.Property(searchArg, "AttributeId");
var searchRight = Expression.Constant(id);
var searchExp = Expression.Equal(searchLeft, searchRight);
Expression<Func<ProductAttribute, bool>> searchExpression = Expression.Lambda<Func<ProductAttribute, bool>>(searchExp, searchArg);
Expression<Func<Product, bool>> subExpression = x => x.ProductAttributes.AsQueryable().Where(searchExpression).FirstOrDefault() != null;
var andExp = Expression.And(expression.Body, subExpression.Body);
expression = Expression.Lambda<Func<Product, bool>>(andExp, arg);
}
var products = context.Products.Include(x => x.ProductAttributes)
.ThenInclude(x => x.Attribute)
.AsNoTracking()
.Where(expression);
// .Where(x => x.ProductAttributes.FirstOrDefault(x => x.AttributeId == attributesIds[0]) != null);
return Ok(products);
The error I get is:
The LINQ expression 'x' could not be translated. 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'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
So, how can I create this expression? Or what is the wrong in my code?
It is not an answer yet, but I think you have to build the following filter, which much more simpler
.Where(x => x.ProductAttributes
.Count(a => attributesIds.Contains(a.AttributeId)) == attributesIds.Length
);
I'm trying to select a subset of two lists of the same object as part of a where clause, and getting an error.
{"Expression of type 'System.Linq.IQueryable1[<>f__AnonymousType772[System.Int32,System.Int32]]' cannot be used for parameter of type 'System.Linq.IQueryable1[Entity.Entities.QuestionModuleQuestionDto]' of method 'System.Linq.IQueryable1[Entity.Entities.QuestionModuleQuestionDto] Where[QuestionModuleQuestionDto](System.Linq.IQueryable1[Entity.Entities.QuestionModuleQuestionDto], System.Linq.Expressions.Expression1[System.Func`2[Entity.Entities.QuestionModuleQuestionDto,System.Boolean]])' (Parameter 'arg0')"}
public async Task<bool> ValidateQuestions(List<QuestionModuleQuestion> questions)
{
var questionModules = await _context.QuestionModule
.Include(qm => qm.Questions)
Where(qm =>
qm.Questions.Select(q => new {q.QuestionId, q.Sequence})
.Intersect(questions.Select(q => new {q.QuestionId, q.Sequence})).Any())
.ToListAsync();
return questionModules.Any(qm => qm.Questions.Count == questions.Count);
}
I rewrote the clause to look like the following, but I'm not too terribly fond of this approach.
var questionModules = await _context.QuestionModule.Include(qm => qm.Questions)
.Where(qm => qm.Questions.Any(qmq => questions.Any(question =>
question.QuestionId == qmq.QuestionId && question.Sequence == qmq.Sequence)))
.ToListAsync();
EF Core has limited support for operations with local collections. You can use only Contains and very simple Any.
You can use this extension FilterByItems and rewrite query in the following way. Query refactored to be effective and I hope that I have not changed requirements.
public async Task<bool> ValidateQuestions(List<QuestionModuleQuestion> questions)
{
var questionCount = questions.Count;
var query = _context.QuestionModule
Where(qm =>
qm.Questions.AsQueryable()
.FilterByItems(questions, (q, i) => q.QuestionId == i.QuestionId && q.Sequence == i.Sequence, true)
.Count() == questionCount);
return await questionModules.AnyAsync();
}
In the following code when I want to get sum for sumField
I'm getting an error(Can Not Convert From Expression<Func<T, decimal>> To Func<T, decimal>)
If I remove the Expression from the groupBy and sumField parameters, my problem will be solved, but in this case, all the data will be sent to the application as IEnumrable and then group by
How can I do this operation Iqueryable??
public virtual async Task<Dictionary<TKey, decimal>> SumAsync<TKey>(
Expression<Func<T, TKey>> groupBy,
Expression<Func<T, decimal>> sumField,
Expression<Func<T, bool>> predicate = null,
List<Expression<Func<T, object>>> includes = null,
string includeString = null)
{
IQueryable<T> query = DbSet;
if (includes != null) query = includes.Aggregate(query, (current, include) => current.Include(include));
if (!string.IsNullOrWhiteSpace(includeString))
{
var incluseSplit = includeString.Split(',');
query = incluseSplit.Aggregate(query, (current, include) => current.Include(include));
}
if (predicate != null) query = query.Where(predicate);
var group = query.GroupBy(groupBy)
.Select(g =>
new
{
Key = g.Key,
SumValue = g.Sum(sumField)
}
)
.AsQueryable();
return group.ToDictionary(s => s.Key, s => s.SumValue);
}
Can you not use a different overload of .GroupBy() as follows:
...
var group = query.GroupBy(groupBy, sumField) // sumField is used to select the elements returned in the grouping
.Select(g =>
new
{
Key = g.Key,
SumValue = g.Sum() // Now only .Sum() is required
}
); // and no .AsQueryable() necessary
...
I would also note that your method is marked async, but does not await anything, so will run synchronously. You might want to use .ToDictionaryAsync() at the end instead.
It is difficult, but possible, to do that without LINQKit. So first variant is with LINQKit, other need dynamic Expression Tree composing.
Activate LINKQKit via DbContextOptions:
builder
.UseSqlServer(connectionString)
.WithExpressionExpanding(); // enabling LINQKit extension
Then we can use LINQKit extension Invoke
public virtual Task<Dictionary<TKey, decimal>> SumAsync<TKey>
(
Expression<Func<T, TKey>> groupBy,
Expression<Func<T, decimal>> sumField,
Expression<Func<T, bool>> predicate = null
)
{
IQueryable<T> query = DbSet;
if (predicate != null)
query = query.Where(predicate);
var group = query
.GroupBy(groupBy)
.Select(g =>
new
{
Key = g.Key,
SumValue = g.Sum(e => sumField.Invoke(e))
}
);
return group.ToDictionaryAsync(s => s.Key, s => s.SumValue);
}
Also removed Includes staff, it is completely ignored by EFCore when you use GroupBy or Select
Can anyone suggest to me which design pattern should I use to handle multiple if conditions for a search function.
The search function takes the product's name, type, and location. In my handler, I handle the input by using if conditions as the example below.
if (!string.isNullOrEmpty(ProductName) && !string.isNullOrEmpty(ProductType))
{
// Query product and return base on name and type.
var product = database.product
.Where(x => x.productname == productname)
.Where(x => x.producttype == producttype)
.ToList()
}
else if (!string.isNullOrEmpty(ProductName)
&& !string.isNullOrEmpty(ProductType)
&& !string.isNullOrEmpty(ProductLocation))
{
// Query product and return base on name and location.
var product = database.product
.Where(x => x.productname == productname)
.Where(x => x.ProductLocation == ProductLocation)
.ToList()
}
So, I ended up having multiples if conditions in my handler. Code starts to get bigger and bigger. In the future, when I may have new types of input. Especially, each if condition will have the same query function and only where the condition is added or removed base on inputs.
Is there a better way to handle inputs and remove duplicated query function?
It is not design pattern but common way when using LINQ
var query = database.product.AsQueryable();
if (!string.IsNullOrEmpty(productName))
query = database.product.Where(x => x.productname == productname);
if (!string.IsNullOrEmpty(productType))
query = database.product.Where(x => x.producttype == producttype);
var product = query.ToList();
Or via helper function:
public static class MyQueryableExtensions
{
public staic IQueryble<T> WhereIf<T>(this IQueryable<T> source, bool condiion, Expression<Func<T, bool> predicate)
{
if (condition)
source = source.Where(predicate);
return source;
}
}
var product = database.product
.WhereIf(!string.IsNullOrEmpty(productName), x => x.productname == productname)
.WhereIf(!string.IsNullOrEmpty(productType), x => x.producttype == producttype)
.ToList();
In short what I want do accomplish is to load Tasks from a project in SharePoint Project Server using CSOM.
var projects = ctx.LoadQuery(ctx.Projects
.Where(p => p.Id == projGuid)
.Include(
p => p.Id, p => p.Name,
p => p.Tasks
.Where(t => t.Id == taskGuid)
.Include(t => t.Name))
);
ctx.ExecuteQuery();
My Problem is with this part .Where(t => t.Id == taskGuid). It works how it should if I only want to load 1 Task but would not work if I want to load more then one. Sure I could write it like that .Where(t => t.Id == taskGuid1 || t.Id == taskGuid2 || ... )
But that wouldn't be dynamic.
What I tried was to use an array and the look if the array GuidArray.Contains(p.Id)
But I get an error if I try to use .Contains() inside the Where() expression.
ClientRequestException: The 'Contains' member cannot be used in the expression.
So I was thinking if it is possible to somehow create the lambda expression based on the number of tasks I want to load.
I regards to creation of lambda, you create the dynamic or condition you are looking for like so
public static class ExpressionExt
{
public static IQueryable<T> Where<T,TKey>(this IQueryable<T> data, string prop,params TKey[] guids)
{
var param = Expression.Parameter(typeof(T));
var exp = guids.Select(g => Expression.Equal(Expression.Property(param, prop), Expression.Constant(g))).Aggregate((a, e) => a != null ? Expression.Or(e, a) : e);
var lambda = Expression.Lambda<Func<T, bool>>(exp, param);
return data.Where(lambda);
}
}
And use it like Where(nameof(A.Id), guids) this is what I usually do when the IQueryable only supports or and not contains. There might a contains implementation there so you might want to check the documentation.