I have a function doing a complicated Where query on my db context and then applies another transformation passed to it:
static IQueryable<T> Query<T>(Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
try {
using (var dbc = new MyDbContext() ) {
var res = dbc.ServicesData
.Where(sd =>
(path1 == null || (path1.Contains("%") || path1.Contains("_") ? EF.Functions.Like(sd.Path1, path1) : sd.Path1 == path1))
&& (path2 == null || (path2.Contains("%") || path2.Contains("_") ? EF.Functions.Like(sd.Path2, path2) : sd.Path2 == path2))
&& (path3 == null || (path3.Contains("%") || path3.Contains("_") ? EF.Functions.Like(sd.Path3, path3) : sd.Path3 == path3))
&& (path4 == null || (path4.Contains("%") || path4.Contains("_") ? EF.Functions.Like(sd.Path4, path4) : sd.Path4 == path4))
&& (path5 == null || (path5.Contains("%") || path5.Contains("_") ? EF.Functions.Like(sd.Path5, path5) : sd.Path5 == path5)));
return f(res.ToList().AsQueryable());
//return f(res).ToList().AsQueryable();
}
} catch (Exception ex_) {
return VList<T>.Empty.AsQueryable();
}
}
This is used ie like this:
IQueryable<int> Int1InLastHour(IQueryable<ServicesData> input) {
var lastHour = DateTimeOffset.Now.AddHours(-1).ToUnixTimeMilliseconds();
return input
.Where(v => (v.Time <= lastHour) && (v.Int1 is object))
.Select(v => v.Int1.Value);
}
var lastHourProcessTime = Query(Int1InLastHour, "Publisher", "%", "ItemProcessTime").Sum();
This works, however since I call res.ToList() before calling f the linq in f is done in memory and not on the DB SQL
If I try to replace f(res.ToList().AsQueryable()) with f(res).ToList().AsQueryable() I get an exception:
{"Processing of the LINQ expression '[EntityShaperExpression][ServicesData]' by 'RelationalProjectionBindingExpressionVisitor' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information."}
Is there any way for me to solve this ? can I somehow pass the query (Func<IQueryable<ServicesData>, IQueryable<T>>) and then combine it to the query in Query before excecuting it on the dbc ?
A few issues. You can split the querying to break down your results, but the scope of your DbContext needs to be at the outermost point of the chain, not inside the inner-most:
This here:
static IQueryable<T> Query<T>(Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
try {
using (var dbc = new MyDbContext() ) { // DbContext should not be scoped here...
var res = dbc.ServicesData
As the simplest re-factor:
static IQueryable<T> Query<T>(MyDbContext dbc, Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
try
{
var res = dbc.ServicesData.AsQueryable();
if(path1 != null)
if(path1.Contains("%") || path1.Contains("_"))
res = res.Where(EF.Functions.Like(sd.Path1, path1));
else
res = res.Where(sd.Path1 == path1);
// Repeat for Path 2 - 5 ....
return f(res);
}
catch (Exception ex_)
{
return VList<T>.Empty.AsQueryable();
}
}
Firstly, we pass in the DbContext. If the context is scoped here, the list must be materialized before being returned. The goal is to allow callers to further reduce the expression before executing the list. This means the DbContext needs to be scoped outside of this initial generation and passed in. With IoC containers managing lifetime scope you can bypass this if the DbContext is injected and scoped to a Request or common lifetime scope.
The next improvement suggestion is to move the conditional checks for the parameters out of the Linq and into regular conditions so that the Like / Equals check will only be added if the condition was provided. This will result in simpler, faster SQL being run on the server.
So the end result would look something like:
using (var dbContext = new MyDbContext())
{
var lastHourProcessTime = Query(dbContext, Int1InLastHour, "Publisher", "%", "ItemProcessTime").Sum();
}
I sort of get where you're trying to go here, but abstracting expressions from EF is bound to lead to confusing code and still prone to limitations and bugs. IMO keeping it simpler generally leads to less issues, but give this a go and see if it gets you closer.
Related
I made a method on the back-end side to handle the filter of my datatable.
On the other hand, this one does not manage the accents of the French language, so if I have "école" and I write "ecole" it cannot find it.
I found this method on another question on stackoverflow
public static String RemoveDiacritics(this String s)
{
String normalizedString = s.Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < normalizedString.Length; i++)
{
Char c = normalizedString[i];
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
and it works, but only for part of my problem. It works on the letter or the word that is written in the search, but I am not able to apply it in my linq query, so with the .RemoveDiacritics() method my "école" becomes "ecole", but I don't am not able to apply it in the column of my table and it always looks for "école".
Here the code for the search:
if (search != null)
{
int n;
search = search.Trim();
var isNumeric = int.TryParse(search, out n);
if (isNumeric)
{
IdFilter = n;
query = query.Where(x => x.UsagerId == IdFilter || x.Niveau == IdFilter);
}
else if (search != "")
{
// this line work
textFilter = search.ToLower().RemoveDiacritics();
// This is the full line, but absolutely takes the accents out to get the right information out
// query = query.Where(x => x.Nom.ToLower().Contains(textFilter) || x.Prenom.ToLower().Contains(textFilter) || x.Username.ToLower().Contains(textFilter) || x.Email.ToLower().Contains(textFilter) || x.EtabNom.ToLower().Contains(textFilter) || x.ActifStatut.ToLower().Contains(textFilter));
// This is the line that will replace the line above, which I try and it doesn't work ( this part: x.Prenom.ToLower().RemoveDiacritics())
query = query.Where(x => x.Prenom.ToLower().RemoveDiacritics().Contains(textFilter));
}
}
This is the basic query:
IQueryable<ListeUsagers> query = (from u in db.USAGERs
join e in db.ETABLISSEMENTs on u.USAGER_INST equals e.ETAB_CODE
where u.USAGER_INST == instId && u.USAGER_NIVEAU > 3 && u.USAGER_NIVEAU < 5 //&& u.USAGER_ACTIF == 1
select new ListeUsagers()
{
UsagerId = u.USAGER_id,
Nom = u.USAGER_NOM,
Prenom = u.USAGER_PRENOM,
EtabCode = e.ETAB_CODE,
EtabNom = e.ETAB_NOM_COURT,
EtabType = e.ETAB_TYPE,
Niveau = u.USAGER_NIVEAU,
Username = u.USAGER_USERNAME,
UserPassword = u.USAGER_MP,
DateCreation = u.USAGER_DATE_INSC,
Sexe = u.USAGER_SEXE,
Lang = u.USAGER_LANGUE,
Telephone = u.USAGER_TELEPHONE,
Email = u.USAGER_EMAIL,
FonctionTravail = u.USAGER_FONCTION,
LieuTravail = u.USAGER_LIEUTRAVAIL,
Note = u.USAGER_NOTE,
Actif = u.USAGER_ACTIF,
ActifStatut = u.USAGER_ACTIF == 0 ? "Inactif" : "Actif"
});
This is the error:
LINQ to Entities does not recognize the method 'System.String RemoveDiacritics(System.String)' method, and this method cannot be translated into a store expression.
There's built-in functionality to do this in entityframework: https://learn.microsoft.com/en-us/ef/core/miscellaneous/collations-and-case-sensitivity if you're using EF 5+
You'll want an accent insensitive collation ("AI", not "AS" in the examples on that page.)
I'm using WCF RIA in a Lightswitch project to create some query results. This query brings back all results regardless. I cannot make it filter the records based on the parameter passed (string Town).
public IQueryable<Enquiries> TestQuery(string Town)
{
List<Enquiries> riaenqs = new List<Enquiries>();
var enqs = this.Context.ClientEnquiries
.Include("Client")
.Include("Client.Town")
.OrderBy(enq => enq.Id);
if (Town != null)
{
enqs.Where(enq => enq.Client.Town.TownName == Town);
}
foreach (ClientEnquiry item in enqs.ToList())
{
Enquiries enq = new Enquiries();
enq.Id = item.Id;
enq.ClientName = item.Client.FirstName + " " + item.Client.Surname;
enq.Town = item.Client.Town != null ? item.Client.Town.TownName : null;
riaenqs.Add(enq);
}
return riaenqs.AsQueryable();
}
During debugging I can see that the Town is correctly populated and I can see that the query is built accordingly if Town is not null. However, when I hit the foreach statement where the linq to ef query is executed I always get all the results. I just cannot figure out where I'm slipping up.
The LINQ methods like the Where do not modify the collection/expression but always returning a new one.
So you need to reassign the result of the Where to your original variable enqs:
if (Town != null)
{
enqs = enqs.Where(enq => enq.Client.Town.TownName == Town);
}
I am getting a null refrence exception when im filtering EF but I am absolultely clueless.
public IEnumerable<TonalityBatchModel> GetTonalityBatch(int briefID)
{
try
{
var brief = NeptuneUnitOfWork.Briefs.FindWhere(b => b.ID == briefID).FirstOrDefault();
if (brief != null && brief.TonalityCriteria != null)
{
return brief.TonalityCriteria.TonalityBatches
.Select(b => new TonalityBatchModel()
{
BriefID = b.BriefID,
Status = b.TonalityCriteria.IsActive == true ?"Active":"Ended",
BatchID = b.ID,
CompetitorID = b.BriefCompetitorID,
Competitor = brief.BriefCompetitors.Where(i=>i.ID == b.BriefCompetitorID).Select(c=>c.Organisation.Name).First(),
Size = b.BatchSize,
StartDate = b.StartDate,
EndDate = b.EndDate,
IsPublished = b.Lookup_TonalityBatchStatus.ID == (int)TonalityBatchStatus.Published?"Yes":"No",
IsCompleted = b.Lookup_TonalityBatchStatus.ID == (int)TonalityBatchStatus.Completed ? "Yes" : "No",
IsAssigned = b.Lookup_TonalityBatchStatus.ID == (int)TonalityBatchStatus.Allocated ? "Yes" : "No",
ImportantCount = b.TonalityItems.Count(i=> i.IsImportant),
ArticlesCount = b.TonalityItems.Count,
FavourableCount = b.TonalityItems.Count(i => i.Lookup_TonalityScoreTypes.ID ==(int)TonalitySourceType.Favourable),
UnfavourableCount = b.TonalityItems.Count(i => i.Lookup_TonalityScoreTypes.ID ==(int)TonalitySourceType.Unfavourable),
NeutralCount = b.TonalityItems.Count(i => i.Lookup_TonalityScoreTypes.ID ==(int)TonalitySourceType.Neutral)
}).ToList();
}
return new List<TonalityBatchModel>();
}
catch (Exception ex)
{
Logger.Error(ex);
throw;
}
}
You'll need to reduce your query to a simpler query, and then start building it back up again until the NullReferenceException occurs. Looking at your code, here are some likely places (I'm making some assumptions since I don't know everything about your model):
Competitor = brief.BriefCompetitors.Where(i=>i.ID == b.BriefCompetitorID).Select(c=>c.Organisation.Name).First()
BriefCompetitors could be null. c.Organisation could be null.
IsPublished = b.Lookup_TonalityBatchStatus.ID == (int)TonalityBatchStatus.Published?"Yes":"No",
(and other similar lines) b.Lookup_TonalityBatchStatus might be null.
ImportantCount = b.TonalityItems.Count(i=> i.IsImportant),
(and other similar lines) b.TonalityItems might be null.
I believe this is because your count is returning null records. I could be wrong but the SQL that's being produced here is something like:
INNER JOIN TonalityItems i on i.Lookup_TonalityScoreTypes == x
Where x is the value of (int)TonalitySourceType.Favourable. Because this join has no matching results there is nothing to do a count on. You could try adding ?? 0 to the end of the query:
FavourableCount = b.TonalityItems.Count(i => i.Lookup_TonalityScoreTypes.ID ==(int)TonalitySourceType.Favourable) ?? 0,
*Mongo Newbie here
I have a document containing several hundred numeric fields which I need to query in combination.
var collection = _myDB.GetCollection<MyDocument>("collection");
IMongoQuery mongoQuery; // = Query.GT("field", value1).LT(value2);
foreach (MyObject queryObj in Queries)
{
// I have several hundred fields such as Height, that are in queryObj
// how do I build a "boolean" query in C#
mongoQuery = Query.GTE("Height", Convert.ToInt16(queryObj.Height * lowerbound));
}
I have several hundred fields such as Height (e.g. Width, Area, Perimeter etc.), that are in queryObj how do I build a "boolean" query in C# that combines range queries for each field in conjunction.
I have tried to use the example Query.GT("field", value1).LT(value2);, however the compiler does not accept the LT(Value) construct. In any event I need to be able to build a complex boolean query by looping through each of the numeric field values.
Thanks for helping a newbie out.
EDIT 3:
Ok, it looks like you already have code in place to build the complicated query. In that case, you just needed to fix the compiler issue. Am assuming you want to do the following (x > 20 && x < 40) && (y > 30 && y < 50) ...
var collection = _myDB.GetCollection<MyDocument>("collection");
var queries = new List<IMongoQuery>();
foreach (MyObject queryObj in Queries)
{
//I have several hundred fields such as Height, that are in queryObj
//how do I build a "boolean" query in C#
var lowerBoundQuery = Query.GTE("Height", Convert.ToInt16(queryObj.Height * lowerbound));
var upperBoundQuery = Query.LTE("Height", Convert.ToInt16(queryObj.Height * upperbound));
var query = Query.And(lowerBoundQuery, upperBoundQuery);
queries.Add(query);
}
var finalQuery = Query.And(queries);
/*
if you want to instead do an OR,
var finalQuery = Query.Or(queries);
*/
Original Answer.
var list = _myDb.GetCollection<MyDoc>("CollectionName")
.AsQueryable<MyDoc>()
.Where(x =>
x.Height > 20 &&
x.Height < 40)
.ToList();
I have tried to use the example Query.GT("field", value1).LT(value2);,
however the compiler does not accept the LT(Value) construct.
You can query MongoDB using linq, if you are using the official C# driver. That ought to solve the compiler issue I think.
The more interesting question I have in mind is, how are you going to construct that complicated boolean query?
One option is to dynamically build an Expression and then pass that to the Where
My colleague is using the following code for something similar...
public static IQueryable<T> Where<T>(this IQueryable<T> query,
string column, object value, WhereOperation operation)
{
if (string.IsNullOrEmpty(column))
return query;
ParameterExpression parameter = Expression.Parameter(query.ElementType, "p");
MemberExpression memberAccess = null;
foreach (var property in column.Split('.'))
memberAccess = MemberExpression.Property
(memberAccess ?? (parameter as Expression), property);
//change param value type
//necessary to getting bool from string
ConstantExpression filter = Expression.Constant
(
Convert.ChangeType(value, memberAccess.Type)
);
//switch operation
Expression condition = null;
LambdaExpression lambda = null;
switch (operation)
{
//equal ==
case WhereOperation.Equal:
condition = Expression.Equal(memberAccess, filter);
lambda = Expression.Lambda(condition, parameter);
break;
//not equal !=
case WhereOperation.NotEqual:
condition = Expression.NotEqual(memberAccess, filter);
lambda = Expression.Lambda(condition, parameter);
break;
//string.Contains()
case WhereOperation.Contains:
condition = Expression.Call(memberAccess,
typeof(string).GetMethod("Contains"),
Expression.Constant(value));
lambda = Expression.Lambda(condition, parameter);
break;
}
MethodCallExpression result = Expression.Call(
typeof(Queryable), "Where",
new[] { query.ElementType },
query.Expression,
lambda);
return query.Provider.CreateQuery<T>(result);
}
public enum WhereOperation
{
Equal,
NotEqual,
Contains
}
Currently it only supports == && !=, but it shouldn't be that difficult to implement >= or <= ...
You could get some hints from the Expression class: http://msdn.microsoft.com/en-us/library/system.linq.expressions.expression.aspx
EDIT:
var props = ["Height", "Weight", "Age"];
var query = _myDb.GetCollection<MyDoc>("CName").AsQueryable<MyDoc>();
foreach (var prop in props)
{
query = query.Where(prop, GetLowerLimit(queryObj, prop), WhereOperation.Between, GetUpperLimit(queryObj, prop));
}
// the above query when iterated over, will result in a where clause that joins each individual `prop\condition` with an `AND`.
// The code above will not compile. The `Where` function I wrote doesnt accept 4 parameters. You will need to implement the logic for that yourself. Though it ought to be straight forward I think...
EDIT 2:
If you don't want to use linq, you can still use Mongo Query. You will just need to craft your queries using the Query.And() and Query.Or().
// I think this might be deprecated. Please refer the release notes for the C# driver version 1.5.0
Query.And(Query.GTE("Salary", new BsonDouble(20)), Query.LTE("Salary", new BsonDouble(40)), Query.GTE("Height", new BsonDouble(20)), Query.LTE("Height", new BsonDouble(40)))
// strongly typed version
new QueryBuilder<Employee>().And(Query<Employee>.GTE(x => x.Salary, 40), Query<Employee>.LTE(x => x.Salary, 60), Query<Employee>.GTE(x => x.HourlyRateToClients, 40), Query<Employee>.LTE(x => x.HourlyRateToClients, 60))
I have a query as follows:
var query = from x in context.Employees
where (x.Salary > 0 && x.DeptId == 5) || x.DeptId == 2
order by x.Surname
select x;
The above is the original query and returns let's say 1000 employee entities.
I would now like to use the first query to deconstruct it and recreate a new query that would look like this:
var query = from x in context.Employees
where ((x.Salary > 0 && x.DeptId == 5) || x.DeptId == 2) && (x,i) i % 10 == 0
order by x.Surname
select x.Surname;
This query would return 100 surnames.
The syntax is probably incorrect, but what I need to do is attach an additional where clause and modify the select to a single field.
I've been looking into the ExpressionVisitor but I'm not entirely sure how to create a new query based on an existing query.
Any guidance would be appreciated. Thanks you.
In an expression visitor you would override the method call. Check if the method is Queryable.Where, and if so, the methods second parameter is a quoted expression of type lambda expression. Fish it out and you can screw with it.
static void Main()
{
IQueryable<int> queryable = new List<int>(Enumerable.Range(0, 10)).AsQueryable();
IQueryable<string> queryable2 = queryable
.Where(integer => integer % 2 == 0)
.OrderBy(x => x)
.Select(x => x.ToString());
var expression = Rewrite(queryable2.Expression);
}
private static Expression Rewrite(Expression expression)
{
var visitor = new AddToWhere();
return visitor.Visit(expression);
}
class AddToWhere : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
ParameterExpression parameter;
LambdaExpression lambdaExpression;
if (node.Method.DeclaringType != typeof(Queryable) ||
node.Method.Name != "Where" ||
(lambdaExpression = ((UnaryExpression)node.Arguments[1]).Operand as LambdaExpression).Parameters.Count != 1 ||
(parameter = lambdaExpression.Parameters[0]).Type != typeof(int))
{
return base.VisitMethodCall(node);
}
return Expression.Call(
node.Object,
node.Method,
this.Visit(node.Arguments[0]),
Expression.Quote(
Expression.Lambda(
lambdaExpression.Type,
Expression.AndAlso(
lambdaExpression.Body,
Expression.Equal(
Expression.Modulo(
parameter,
Expression.Constant(
4
)
),
Expression.Constant(
0
)
)
),
lambdaExpression.Parameters
)
)
);
}
}
}