I am trying to map a front end query builder to a backend ORM (OrmLite).
For instance, the front end might send 3 string values: SomeField, = foo.
If I want to generate this query in the ORM I would do:
var q = db.From<MyEntity>()
.Where(x => x.SomeField == "foo")
So what I need to do is come up with a way to build the where predicate from a string.
I can see the type Where is expecting is: Expression<Func<MyEntity,bool>>
So I think I need something like:
var q = db.From<MyEntity>()
.Where(GetQueryPart("SomeField", "=", "foo");
//....
public Expression<Func<T,bool>> GetQueryPart<T>(string field, string queryOperator, string value)
{
//...
}
But I am not exactly sure if this is the right approach or where exactly to start. I had a look through the docs here https://learn.microsoft.com/en-us/dotnet/api/system.linq.expressions.expression?view=net-5.0 and it is not clear to me how to build an expression.
Can someone give me some help with how I should approach this?
Related
I'm trying to create a generic blazor component that and has a config object.
So far I've been able to successfully mimic .Where(), .Include().ThenInclude() separately, but now I need to add condition to the include.
My config file has the following structure
class Config
{
public List<string> includes {get;set;}
public List<Expression<Func<T,bool>>> conditions {get;set;}
}
database structure
For the example lets assume the following structure.
Dataset (1)<--(n) DatasetItem
DatasetItem (1)-->(1) Image
DatasetItem (1)-->(1) Label
Image (1)<--(n) Labels
Label(1)<--(n) Tags
where an image can be a part of more than one dataset and it's tags might or might not be included in the same dataset. Imagine an picture of a city park with dogs, people, and cars. Someone would want a dataset of vehicles and consider only the cars and ignore the labels about animals and people.
A Label include the coordinates and one or more TagNames (vehicle, car, red, fourWheeler, etc)
what works
And I can give conditions and includes this way:
var config = new Config<DatasetItem>()
{
includes = new() {"Image", "Image.Labels", "Image.Labels.Tags"}
conditions = new() {di => di.datasetId == 1, di => di.Label.width > 100}
}
the problems and limitations of this approach
For this to work I use the .Include(str) overload, but I don't like that approach since it is prone to fail.
The other problem with this approach is a limitation on the conditions.
For example, I can not add a condition to get only the Tags that belong to a specific category.
It seems that is not possible to add a condition that would give me this filter. Best I got was with the use of All or Any, but those do not give the desire result.
config.conditions.Add(
di => di.Images.Any(i => Labels.Any(l => l.Tags.Any(t => t.Name == "vehicle"))
)
what I'm looking for
Whith regular use of Include, obtaining the desire result would be like this.
DatasetItems.Include(di => di.Images)
.ThenInclude(i => i.Labels)
.ThenInclude(l => l.Tags.Where(t => t.Name == "vehicle"))
So, is there any way to achieve this?
I thing the solution would require to use Expression Trees, Reflection, and Recursive functions using MakeGenericMethod, but I havent found a way to achive this.
the ideal solution would be to find a structure like the one for conditions
List<Expression<Func<T,something>>> that lets me add the condition in this way
includes.add(di => di.Images.Labels.Tags.Where(t => t.Name == "vehicle"))
a failed attempt
I'm including a failed attempt I took to solve this, just in case this helps to narrow down the intention and for if someone can use it for solving the problem. This attempt is far from ideal.
property was a dot-separated string, like "Image.Labels.Tags" and filter was suppoused to be the Where expression.
This function does not compile.
public static IQueryable<T> FilteredInclude<T>(this IQueryable<T> query, string property, Func<dynamic, bool> filter) where T : class
{
var type = typeof(T);
var properties = property.Split('.');
var propertyInfo = type.GetProperty(properties[0]);
var parameter = Expression.Parameter(type, "x");
var propertyExpression = Expression.Property(parameter, propertyInfo);
var lambda = Expression.Lambda(propertyExpression, parameter);
var filteredQuery = query.Where(x => filter(propertyInfo.GetValue(x)));
if (properties.Length == 1)
{
var methodInfo = typeof(QueryableExtensions).GetMethod(nameof(Include));
methodInfo = methodInfo.MakeGenericMethod(type, propertyInfo.PropertyType);
var result = methodInfo.Invoke(null, new object[] { filteredQuery, lambda });
return (IQueryable<T>)result;
}
else
{
var nextProperty = string.Join(".", properties.Skip(1));
var result = filteredQuery.FilteredInclude(nextProperty, filter);
var thenIncludeMethod = typeof(Microsoft.EntityFrameworkCore.Query.Internal.IncludeExpression)
.GetMethod("ThenInclude")
.MakeGenericMethod(propertyInfo.PropertyType);
var resultWithThenInclude = thenIncludeMethod.Invoke(result, new object[] { lambda });
return (IQueryable<T>)resultWithThenInclude;
}
}
I have the following code (this is just relevant part):
linqQuery.Select(invoice =>
new InvoiceDetailed
{
UnpaidAmount = e.SumAmount +
e.OverdueNotices.OrderByDescending(on => on.SendDate).Select(on => on.Fee).DefaultIfEmpty(0).Sum() +
e.CreditNotes.Select(c => c.CreditNoteAmount).DefaultIfEmpty(0).Sum() -
e.Payments.Select(p => p.Amount).DefaultIfEmpty(0).Sum()
}
And this calculation for UnpaidAmount I repeat in severl other queries also. My question is if there is a way to somehow wrap that expression in function like:
Expression<Func<crmInvoice_Invoice, double>> unpaidExpression = // that unpaid amount caluculation expression
And then call like this:
linqQuery.Select(invoice =>
new InvoiceDetailed
{
UnpaidAmount = unpaidExpression(invoice)
}
Then I could reuse it in more queries. Is it possible to do something similar in LINQ? And if it is not is there any alternative solution u could suggest me to avoid repeating that part of code?
No, it's impossible.
Select method gets Expression as an argument. LINQ to SQL parses Expression to SQl code. So, to solve your task you need to convert you expression to return InvoiceDetailed:
Expression<Func<crmInvoice_Invoice, InvoiceDetailed>> InvoiceDetailedExpression = ...
I want do something like this:
string test = alarmType;
db.Alarms.Where(alarmType.Contains(m => m.Type)).ToList();
But this doesn't work. How can I make such query? Is it the only way to use pure SQL?
UPD
I'm trying to find whether records is substring of the "test", not conversly.
You have to reverse the condition:
var query = db.Alarms
.Where(a => alarmType.Contains(a.Type))
.ToList();
However, your code sample is confusing, if alarmType is a string i don't know what you're trying to achieve.
string test = alarmType;
Update: if you're using LINQ-To-Sql and you want to find all records where the Type is a substring of alarmType you can use:
var query = db.Alarms
.Where(a => SqlMethods.Like(alarmType, string.Format("%{0}%", a.Type)))
.ToList();
Try the following
string test = alarmType;
var result = db.Alarms.Where(m => alarmType.Contains(m.Type)).ToList();
Your LINQ query isn't well-formatted. You have:
db.Alarms.Where(alarmType.Contains(m => m.Type)).ToList();
So the parameter you've passed to Contains is a lambda, which isn't what Contains takes,
Likeiwse, Contains returns a bool, so you've passed a bool to Where, which is also not a parameter type it takes.
What you want is to pass a lambda to Where, like so:
db.Alarms.Where(m => alarmType.Contains(m.Type)).ToList();
Note how now both the Where and the Contains are being passed parameters of the correct type.
Let's say you have the following code:
string encoded="9,8,5,4,9";
// Parse the encoded string into a collection of numbers
var nums=from string s in encoded.Split(',')
select int.Parse(s);
That's easy, but what if I want to apply a lambda expression to s in the select, but still keep this as a declarative query expression, in other words:
string encoded="9,8,5,4,9";
// Parse the encoded string into a collection of numbers
var nums=from string s in encoded.Split(',')
select (s => {/* do something more complex with s and return an int */});
This of course does not compile. But, how can I get a lambda in there without switching this to fluent syntax.
Update: Thanks to guidance from StriplingWarrior, I have a convoluted but compilable solution:
var result=from string s in test.Split(',')
select ((Func<int>)
(() => {string u="1"+s+"2"; return int.Parse(u);}))();
The key is in the cast to a Func<string,int> followed by evaluation of the lambda for each iteration of the select with (s). Can anyone come up with anything simpler (i.e., without the cast to Func followed by its evaluation or perhaps something less verbose that achieves the same end result while maintaining the query expression syntax)?
Note: The lambda content above is trivial and exemplary in nature. Please don't change it.
Update 2: Yes, it's me, crazy Mike, back with an alternate (prettier?) solution to this:
public static class Lambda
{
public static U Wrap<U>(Func<U> f)
{
return f();
}
}
...
// Then in some function, in some class, in a galaxy far far away:
// Look what we can do with no casts
var res=from string s in test.Split(',')
select Lambda.Wrap(() => {string u="1"+s+"2"; return int.Parse(u);});
I think this solves the problem without the ugly cast and parenarrhea. Is something like the Lambda.Wrap generic method already present somewhere in the .NET 4.0 Framework, so that I do not have to reinvent the wheel? Not to overburden this discussion, I have moved this point into its own question: Does this "Wrap" generic method exist in .NET 4.0.
Assuming you're using LINQ to Objects, you could just use a helper method:
select DoSomethingComplex(s)
If you don't like methods, you could use a Func:
Func<string, string> f = s => { Console.WriteLine(s); return s; };
var q = from string s in new[]{"1","2"}
select f(s);
Or if you're completely hell-bent on putting it inline, you could do something like this:
from string s in new[]{"1","2"}
select ((Func<string>)(() => { Console.WriteLine(s); return s; }))()
You could simply do:
var nums = from string s in encoded.Split(',')
select (s => { DoSomething(); return aValueBasedOnS; });
The return tells the compiler the type of the resulting collection.
How about this:
var nums= (from string s in encoded.Split(',') select s).Select( W => ...);
Can anyone come up with anything
simpler?
Yes. First, you could rewrite it like this
var result = from s in encoded.Split(',')
select ((Func<int>)(() => int.Parse("1" + s + "2")))();
However, that's not really readable, particularly for a query expression. For this particular query and projection, the let keyword could be used.
var result = from s in encoded.Split(',')
let t = "1" + s + "2"
select int.Parse(t);
IEnumerable integers = encoded.Split(',').Select(s => int.Parse(s));
Edit:
IEnumerable<int> integers = from s in encoded.Split(',') select int.Parse(string.Format("1{0}2",s));
I am trying to do a like comparison based on an outside parameter (passed by a search form) that determines type of comparison ("%string" or "string%" or "%string%")
I was thinking in the following direction:
query = query.Where(
Entity.StringProperty.Like("SearchString", SelectedComparsionType)
)
Like method would than based on selected type return
.StartsWith() or .EndsWith() or .SubString()
My knowledge of expressions is apparently far from great, since i haven't been able to construct a method that could yield the right result (server side comparison in SQL just like with StartsWith method).
The easy way
Just use
if (comparison == ComparisonType.StartsWith)
query = query.Where(e => e.StringProperty.StartsWith("SearchString"));
else if ...
The hard way
If you want to do something like this, either make sure your LINQ provider can be told of this new method somehow, and how it translates to SQL (unlikely), or prevent your method from ever reaching the LINQ provider, and provide the provider something it understands (hard). For example, instead of
query.Where(e => CompMethod(e.StringProperty, "SearchString", comparsionType))
you can create something like
var query = source.WhereLike(e => e.StringProperty, "SearchString", comparsionType)
with the following code
public enum ComparisonType { StartsWith, EndsWith, Contains }
public static class QueryableExtensions
{
public static IQueryable<T> WhereLike<T>(
this IQueryable<T> source,
Expression<Func<T, string>> field,
string value,
SelectedComparisonType comparisonType)
{
ParameterExpression p = field.Parameters[0];
return source.Where(
Expression.Lambda<Func<T, bool>>(
Expression.Call(
field.Body,
comparisonType.ToString(),
null,
Expression.Constant(value)),
p));
}
}
You can even add additional criteria this way
var query = from e in source.WhereLike(
e => e.StringProperty, "SearchString", comparsionType)
where e.OtherProperty == 123
orderby e.StringProperty
select e;
The very, very hard way
It would (technically) be possible to rewrite the expression tree before the provider sees it, so you can use the query you had in mind in the first place, but you'd have to
create a Where(this IQueryable<EntityType> source, Expression<Func<EntityType, bool>> predicate) to intercept the Queryable.Where,
rewrite the expression tree, replacing your CompMethod, wherever it is, with one of the String methods,
call the original Queryable.Where with the rewritten expression,
and first of all, be able to follow the extension method above in the first place!
But that's probably way too complicated for what you had in mind.
Sounds like you should be wanting to use:
query = query.Where(
Entity.StringProperty.Contains("SearchString")
)
This should map to:
WHERE StringProperty LIKE '%SearchString%'
This should also work for more advanced search masks such as "Mr? Sm%th", but I haven't had to test any search strings like that myself yet.
UPDATE: Based on OPs edit
It sounds like what you are asking for is something like the following:
public enum SelectedComparsionType
{
StartsWith,
EndsWith,
Contains
}
public static bool Like(this string searchString, string searchPattern, SelectedComparsionType searchType)
{
switch (searchType)
{
case SelectedComparsionType.StartsWith:
return searchString.StartsWith(searchPattern);
case SelectedComparsionType.EndsWith:
return searchString.EndsWith(searchPattern);
case SelectedComparsionType.Contains:
default:
return searchString.Contains(searchPattern);
}
}
This would allow you to write code as you require, i.e:
query = query.Where(
Entity.StringProperty.Like("SearchString", SelectedComparsionType.StartsWith)
)
However, personally, I would replace any use of SelectedComparsionType, with a direct call to the required string function. I.e
query = query.Where(
Entity.StringProperty.StartsWith("SearchString")
)
As this will still map to a SQL 'LIKE' query.
This is exactly what I had in mind, thank you. I had something similar already written, but it didn't translate to SQL. For example, it worked if I did this directly:
Entity.StringProperty.EndsWith("SearchString");
It didn't work if I used a dedicated method:
CompMethod("BaseString","SearchString",SelectedComparsionType.EndsWith)
I think it probably has something to do with expression evaluation, i'm just not sure what.
You will be better off using Regex to solve this problem.