Using Custom Extension Methods inside LinQ (Compare two strings) - c#

I wanna create a comparer for LinQ that is simplified. this is my current code.
DBContext.product_type
.Where(x => x.name.ToLower()(filter.ToLower())
|| x.description..ToLower()(filter.ToLower())
|| x.online_category.description..ToLower()(filter.ToLower()))
I wanna simplify it using linQ extension method. So far this is what I got.
DBContext.product_type
.Where(x => x.name.CompareToLower(filter)
|| x.description.CompareToLower(filter)
|| x.online_category.description.CompareToLower(filter))
public static bool CompareToLower(this string str, string comparer)
{
if (str.ToLower().Contains(comparer.ToLower()))
{
return true;
}
else
{
return false;
}
}
This is the error im getting
linq to Entities does not recognize the method 'Boolean CompareToLower(System.String, System.String)' method, and this method cannot be translated into a store expression.

Extension methods like that cannot be converted to SQL Query.
The answer provided by #meysamasadi will work, but will fetch your entire table and do the comparison on client side.
If you want the comparison to occur on the database-side, the most efficient way would be to change your collation to case-insensitive (as suggested in the comments).
As an alternative you can use following code:
DBContext.product_type
.Where(x =>
EF.Functions.Like(x.Name, filter)
|| EF.Functions.Like(x.Description, filter)
|| EF.Functions.Like(x.online_category.description, filter))
** Note that using the above method you can also include wildcards in the filter and patterns
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbfunctionsextensions.like?view=efcore-5.0

Related

Linq.Dynamic FirstOrDefault() nested in OrderBy

I have the following Linq statement, which works totally fine:
query = query.OrderBy(m => m.MATERIAL_TXT.Where(mt => mt.LANG == "EN").FirstOrDefault().LTEXT);
Now I'm trying to make it dynamic by using the string based syntax from Linq.Dynamic:
query = query.OrderBy("MATERIAL_TXT.Where(LANG==\"EN\").FirstOrDefault().LTEXT");
But it throws the exception :
"No applicable aggregate method 'FirstOrDefault' exists"
It has to bedynamic so that it accepts other names instead of "MATERIAL_TXT".
What am I missing?
According to the documentation:
A subset of the Standard Query Operators is supported for objects that implement IEnumerable. Specifically, the following constructs are permitted, where seq is an IEnumerable instance, predicate is a boolean expression, and selector is an expression of any type:
seq.Where(predicate)
seq.Any()
seq.Any(predicate)
seq.All(predicate)
seq.Count()
seq.Count(predicate)
seq.Min(selector)
seq.Max(selector)
seq.Sum(selector)
seq.Average(selector)
FirstOrDefault isn't on the list, so it's reasonably safe to assume it isn't supported.
You can't use FirstOrDefault as string like that.
if you want to create dynamic orderBy try this :
Func<IQueryable<YourEntityType>, IOrderedQueryable<YourEntityType>> orderBy;
orderBy = x => x.OrderBy(m => m.MATERIAL_TXT.Where(mt => mt.LANG == "EN").FirstOrDefault().LTEXT);
Then you can use it like this :
orderBy(query);
for example you can use it in another method :
public List<YourEntityType> YourMethodName(Func<IQueryable<YourEntityType>, IOrderedQueryable<YourEntityType>> orderBy,IQueryable<YourEntityType> query=null)
{
query=query ?? GetYourEntityTypeList().AsQueryable();
return orderBy(query).ToList();
}
I Hope it will be useful .

Combining expression trees

I have the following expression:
public Expression<Func<T, bool>> UserAccessCheckExpression<T>(int userId) where T : class
{
return x => (IsAdmin || userId == CurrentUserId || userId == 0);
}
Then I want to apply this filter to several collections (IQueryable) like this one:
return tasks
.Where(t => t.TaskUsers
.Any(x => UserAccessCheckExpression<TaskUser>(x.User) && x.SomeBool == true));
I'm getting the following error while doing so:
Error 40 Cannot implicitly convert type System.Linq.Expressions.Expression<System.Func<TaskUser,bool>> to bool
I can't use workaround with interface inheritance (like TaskUser inherits interface with int UserId property (where T : IHasUserId)) since I want to combine logic.
The problem is that your UserAccessCheckExpression() method is returning an Expression while the Any() method is expecting a boolean.
Now, you can get your code to compile by compiling the Expression and invoking the method (using UserAccessCheckExpression<TaskUser>(x.User).Compile().Invoke(x.User)) but that would obviously fail on runtime because Linq-to-Entities wouldn't be able to translate your Any() to a store query as it no longer contains an Expression.
LinqKit is aiming to solve this problem using its own Invoke extension method that while letting your code compile, will make sure your Expression will get replaced back to its original form using another extension method named AsExpandable() that is extending the entity set.
Try this:
using LinqKit.Extensions;
return tasks
.AsExpandable()
.Where(t => t.TaskUsers.Any(
x => UserAccessCheckExpression<TaskUser>(x.User).Invoke(x)
&& x.SomeBool == true));
More on LinqKit
Yeah, so, you can't do that. There's a difference between an Expression<> and a Func<>. You're trying to use the UserAccessCheckExpression as a func. I'm not sure what you're trying to do, but you can compile it to a func and then use it sorta like you are:
var expr = UserAccessCheckExpression<TaskUser>(x.User);
var func = expr.Compile();
// Later use it like ...
var result = func();
But I expect you're using this with EF or Linq2Sql? That being the case you'll need to rewrite the expression. It can be done by hand (not easy) or, better, use a tool like PredicateBuilder.

Passing Parameters into an Expression Specification using LinqToSQL

I want to reduce duplicate logic in a LinqToSQL query by using Expression<Func<T,bool>>. We've successfully done the before using static properties like so:
public static Expression<Func<Document, bool>> IsActive
{
get
{
return document => !document.Deleted;
}
}
...
_workspace.GetDataSource<Document>().Where(DocumentSpecifications.IsActive)
However I am struggling to get this working when additional parameters need to be passed into the Expression like so:
public static Expression<Func<Comment, bool>> IsUnread(int userId, Viewed viewed)
{
return
c =>
!c.Deleted && c.CreatedByActorID != actorId
&& (viewed == null || c.ItemCreatedDate > viewed.LastViewedDate);
}
...
// Throwing "Argument type 'System.Linq.Expression<X,bool>' is not assignable
// to parameter type 'System.Func<X,bool>'"
return (from a in alerts
select
new UnreadComments
{
TotalNumberOfUnreadComments =
a.Comments.Count(CommentSpecifications.IsUnread(actorId, a.LastView))
})
How do I convert the specification so it can be accepted in this way and would it still convert to SQL correctly?
EDIT: Following Anders advice I added .Compile() to the query. It now works correctly when unit testing in memory collections; however when LinqToSQL trys to convert it into SQL I get the following exception:
System.NotSupportedException: Unsupported overload used for query operator 'Count'
I've tried:
a.Comments.Count(CommentSpecifications.IsUnread(actorId, a.LastView).Compile())
a.Comments.AsQueryable().Count(CommentSpecifications.IsUnread(actorId, a.LastView))
It looks like the second query is executed as linq-to-objects and not as linq-to-sql. It expects a Func<X, bool> which is what linq-to-objects use, while linq-to-sql (or any other IQueryable provider) expects an uncompiled expression tree that can be translated to something else)
A quick fix is to call Compile() on the expression to convert it to an executable function.
a.Comments.Count(CommentSpecifications.IsUnread(actorId, a.LastView).Compile())
To be more detailed you really should figure out why that query is executed as linq-to-objects and not linq-to-sql. Especially if you expected it to be translated to efficient sql it could become a performance nightmare.
Update
After your edit it's more obvious what's happening:
You're running the query as linq-to-objects during unit testing and as linq-to-sql later. In that case converting the expression to a Func<> through Compile() won't work as linq-to-sql won't recognize it.
Update 2
Composing reusable part into query expression that are to be translated is hard - it confuses the translation engine. Linq-to-sql is somewhat more tolerant than linq-to-entities is, but it is nevertheless hard to get it work. A better way is often to make chaining functions that operate on IQueryable<T>.
public static IQueryable<Comment> WhereIsUnread(this IQueryable<Comment> src, int userId)
{
return src.Where(
c =>
!c.Deleted && c.CreatedByActorID != actorId
&& (viewed == null || c.ItemCreatedDate > c.Alert.LastView.LastViewedDate));
}
...
return (from a in alerts
select
new UnreadComments
{
TotalNumberOfUnreadComments =
a.Comments.WhereIsUnRead(actorId).Count()
})
Something like that should work. Notice I've rewritten how the last viewed date is accessed, as it would otherwise fail translation to SQL when passed in as a parameter.

String.IsNullOrWhiteSpace in LINQ Expression

I have the following code:
return this.ObjectContext.BranchCostDetails.Where(
b => b.TarrifId == tariffId && b.Diameter == diameter
|| (b.TarrifId==tariffId && !string.IsNullOrWhiteSpace(b.Diameter))
|| (!b.TarrifId.HasValue) && b.Diameter==diameter);
And I get this error when I try to run the code:
LINQ to Entities does not recognize the method 'Boolean
IsNullOrWhiteSpace(System.String)' method, and this method cannot be
translated into a store expression."
How can I solve this problem and write code better than this?
You need to replace
!string.IsNullOrWhiteSpace(b.Diameter)
with
!(b.Diameter == null || b.Diameter.Trim() == string.Empty)
For Linq to Entities this gets translated into:
DECLARE #p0 VarChar(1000) = ''
...
WHERE NOT (([t0].[Diameter] IS NULL) OR (LTRIM(RTRIM([t0].[Diameter])) = #p0))
and for Linq to SQL almost but not quite the same
DECLARE #p0 NVarChar(1000) = ''
...
WHERE NOT (LTRIM(RTRIM([t0].[TypeName])) = #p0)
In this case it is important to distinguish between IQueryable<T> and IEnumerable<T>. In short IQueryable<T> is processed by a LINQ provider to deliver an optimized query. During this transformation not all C# statements are supported, as it either is not possible to translate them to a back-end specific query (e.g. SQL) or because the implementer did not foresee the need for the statement.
In contrast IEnumerable<T> is executed against the concrete objects and, therefore, will not be transformed. So, it is quite common that constructs, which are useable with IEnumerable<T>, cannot be used with IQueryable<T> and also that IQueryables<T> backed by different LINQ providers do not support the same set of functions.
However, there are some workarounds (like Phil's answer), which modify the query. Also, as a more general approach it is possible to drop back to an IEnumerable<T> before continuing with the specification of the query. This, however, might have a performance hit - especially when using it on restrictions (e.g. where clauses). In contrast, when dealing with transformations the performance hit is a lot smaller, sometimes even non existent - depending on your query.
So the above code could also be rewritten like this:
return this.ObjectContext.BranchCostDetails
.AsEnumerable()
.Where(
b => b.TarrifId == tariffId && b.Diameter == diameter
|| (b.TarrifId==tariffId && !string.IsNullOrWhiteSpace(b.Diameter))
||(!b.TarrifId.HasValue) && b.Diameter==diameter
);
NOTE: Ths code will have an higher performance impact than Phil's answer. However, it shows the principle.
Use an expression visitor to detect references to string.IsNullOrWhiteSpace and break them down into a simpler expression (x == null || x.Trim() == string.Empty).
So below is an extended visitor and an extension method to make use of it. It requires no special config to use, simply call WhereEx instead of Where.
public class QueryVisitor: ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.IsStatic && node.Method.Name == "IsNullOrWhiteSpace" && node.Method.DeclaringType.IsAssignableFrom(typeof(string)))
{
//!(b.Diameter == null || b.Diameter.Trim() == string.Empty)
var arg = node.Arguments[0];
var argTrim = Expression.Call(arg, typeof (string).GetMethod("Trim", Type.EmptyTypes));
var exp = Expression.MakeBinary(ExpressionType.Or,
Expression.MakeBinary(ExpressionType.Equal, arg, Expression.Constant(null, arg.Type)),
Expression.MakeBinary(ExpressionType.Equal, argTrim, Expression.Constant(string.Empty, arg.Type))
);
return exp;
}
return base.VisitMethodCall(node);
}
}
public static class EfQueryableExtensions
{
public static IQueryable<T> WhereEx<T>(this IQueryable<T> queryable, Expression<Func<T, bool>> where)
{
var visitor = new QueryVisitor();
return queryable.Where(visitor.VisitAndConvert(where, "WhereEx"));
}
}
So if you run myqueryable.WhereEx(c=> !c.Name.IsNullOrWhiteSpace()) it will be converted to !(c.Name == null || x.Trim() == "") before being passes to whatever (linq to sql/entities) and converted to sql.
You can also use this to check for whitespace:
b.Diameter!=null && !String.IsNullOrEmpty(b.Diameter.Trim())
!String.IsNullOrEmpty(b.Diameter.Trim())
will throw exception if b.Diameter is null.
If you still want to use your statement, better use this check
!String.IsNullOrWhiteSpace(b.Diameter), IsNullOrWhiteSpace = IsNullOrEmpty + WhiteSpace

How to use LINQ Contains() to find a list of enums?

I have an enum called OrderStatus, and it contains various statuses that an Order can be in:
Created
Pending
Waiting
Valid
Active
Processed
Completed
What I want to do is create a LINQ statement that will tell me if the OrderStaus is Valid, Active, Processed or Completed.
Right now I have something like:
var status in Order.Status.WHERE(status =>
status.OrderStatus == OrderStatus.Valid ||
status.OrderStatus == OrderStatus.Active||
status.OrderStatus == OrderStatus.Processed||
status.OrderStatus == OrderStatus.Completed)
That works, but it's very "wordy". Is there a way to convert this to a Contains() statement and shorten it up a bit?
Sure:
var status in Order.Status.Where(status => new [] {
OrderStatus.Valid,
OrderStatus.Active,
OrderStatus.Processed,
OrderStatus.Completed
}.Contains(status.OrderStatus));
You could also define an extension method In() that would accept an object and a params array, and basically wraps the Contains function:
public static bool In<T>(this T theObject, params T[] collection)
{
return collection.Contains(theObject);
}
This allows you to specify the condition in a more SQL-ish way:
var status in Order.Status.Where(status =>
status.OrderCode.In(
OrderStatus.Valid,
OrderStatus.Active,
OrderStatus.Processed,
OrderStatus.Completed));
Understand that not all Linq providers like custom extension methods in their lambdas. NHibernate, for instance, won't correctly translate the In() function without additional coding to extend the expression parser, but Contains() works just fine. For Linq 2 Objects, no problems.
I have used this extension:
public static bool IsIn<T>(this T value, params T[] list)
{
return list.Contains(value);
}
You may use this as the condition:
Where(x => x.IsIn(OrderStatus.Valid, ... )
If that set of statuses has some meaning, for example those are statuses for accepted orders, you can define an extension method on your enum and use that in your linq query.
public static class OrderStatusExtensions
{
public static bool IsAccepted(this OrderStatuses status)
{
return status == OrderStatuses.Valid
|| status == OrderStatuses.Active
|| status == OrderStatuses.Processed
|| status == OrderStatuses.Completed;
}
}
var acceptedOrders = from o in orders
where o.Status.IsAccepted()
select o;
Even if you could not give the method a simple name, you could still use something like IsValidThroughCompleted. In either case, it seems to convey a little more meaning this way.
Assumnig that the enum is defined in the order you specified in the question, you could shorten this by using an integer comparison.
var result =
Order.Status.Where(x =>
(int)x >= (int)OrderStatus.Valid &
& (int)x <= (int)OrderStatus.Completed);
This type of comparison though can be considered flaky. A simply re-ordering of enumeration values would silently break this comparison. I would prefer to stick with the more wordy version and probably clean up it up by refactoring out the comparison to a separate method.
You could put these in a collection, and use:
OrderStatus searchStatus = new[] {
OrderStatus.Valid,
OrderStatus.Active,
OrderStatus.Processed,
OrderStatus.Completed };
var results = Order.Status.Where(status => searchStatus.Contains(status));

Categories

Resources