D365 CE: difference between OrganizationServiceContext and QueryExpression through Service? - plugins

I'm trying to execute a LINQ query within a plugin, using the OrganizationServiceContext, to retrieve some quotes. On these quotes, I'm using .Select() to only select the value for the field cgk_totalnetprice, as shown below:
quotes = OrganizationServiceContext.QuoteSet
.Where(_ =>
_.OpportunityId != null &&
_.OpportunityId.Id == opportunityId &&
_.QuoteId != currentQuote.Id &&
(_.StatusCode.Value == (int)Quote_StatusCode.Won || _.StatusCode.Value == (int)Quote_StatusCode.WonOrder) &&
_.cgk_quotetypecode != null &&
(_.cgk_quotetypecode.Value == (int)QuoteTypeCode.Regular || _.cgk_quotetypecode.Value == (int)QuoteTypeCode.ServiceUnderWarranty))
.Select(x => new Quote() { Id = x.Id, cgk_totalnetprice = x.cgk_totalnetprice})
.ToList();
However, when retrieving those quotes, the context does not return a value for all except one quote (and it is not the quote that triggered the update in the first place, but just a random one that was not updated at all)
Weird part: when I rewrite the query to a QueryExpression, everything works perfectly:
QueryExpression qe = new QueryExpression("quote");
//Exclude current quote
qe.Criteria.AddCondition("quoteid", ConditionOperator.NotEqual, currentQuote.Id);
//Opportunity
qe.Criteria.AddCondition("opportunityid", ConditionOperator.NotNull);
qe.Criteria.AddCondition("opportunityid", ConditionOperator.Equal, opportunityId);
//State-Status
FilterExpression statusFilter = new FilterExpression(LogicalOperator.Or);
statusFilter.AddCondition("statuscode", ConditionOperator.Equal, (int)Quote_StatusCode.Won);
statusFilter.AddCondition("statuscode", ConditionOperator.Equal, (int)Quote_StatusCode.WonOrder);
qe.Criteria.AddFilter(statusFilter);
//QuoteType
qe.Criteria.AddCondition("cgk_quotetypecode", ConditionOperator.NotNull);
FilterExpression typeFilter = new FilterExpression(LogicalOperator.Or);
typeFilter.AddCondition("cgk_quotetypecode", ConditionOperator.Equal, (int)QuoteTypeCode.Regular);
typeFilter.AddCondition("cgk_quotetypecode", ConditionOperator.Equal, (int)QuoteTypeCode.ServiceUnderWarranty);
qe.Criteria.AddFilter(typeFilter);
qe.ColumnSet = new ColumnSet("quoteid", "cgk_totalnetprice");
quotes = this.OrganizationService.RetrieveMultiple(qe).Entities.Cast<Quote>().ToList();
What could cause this difference between OrganizationServiceContext and OrganizationService + QueryExpression??

Queries on OrganizationServiceContext rely on LINQ for CRM, which in turn translates LINQ expressions into QueryExpression objects. LINQ for CRM comes with a few weaknesses:
it does not implement all capabilities of the underlying QueryExpression,
it only supports a limited set of LINQ constructs (see MS Docs),
in some cases it creates incorrect queries,
query processing is approx. 10% slower.
Your query looks pretty straightforward, yet it fails. Maybe you can leave the line _.cgk_quotetypecode != null && out. I guess it is not needed and combined with the subsequent filtering on the same attribute it may trick the LINQ parser into constructing the wrong filter and/or conditions.
Another option is to materialize the LINQ query first and then select the columns needed. Of course this will lead to a select *, but it's often worth trying while troubleshooting.
E.g. you could write:
.ToArray()
.Select(x => new Quote() { Id = x.Id, cgk_totalnetprice = x.cgk_totalnetprice});
Working with Dynamics CRM/365 CE I learned to avoid LINQ for CRM. Instead I use a bunch of extension methods allowing me to create QueryExpression queries in a much less verbose way.
Final suggestion: in some cases a filter's LogicalOperator.Or can be replaced by ConditionOperator.In or ConditionOperator.Between. Doing so the construct
//State-Status
FilterExpression statusFilter = new FilterExpression(LogicalOperator.Or);
statusFilter.AddCondition("statuscode", ConditionOperator.Equal, (int)Quote_StatusCode.Won);
statusFilter.AddCondition("statuscode", ConditionOperator.Equal, (int)Quote_StatusCode.WonOrder);
qe.Criteria.AddFilter(statusFilter);
can simply be replaced by this oneliner:
qe.Criteria.AddCondition("statuscode", ConditionOperator.In, (int)Quote_StatusCode.Won, (int)Quote_StatusCode.WonOrder);

I generally use the query syntax for Dynamics LINQ queries. I'd suggest standard query troubleshooting - start with no conditions and add them one-by-one.
Below is an idea of how my query would look. I'd keep adding / modifying the where clauses until the query returned what I expected.
We could use && operators instead of multiple where clauses, but I find that having the multiple where clauses often makes commenting and uncommenting easier.
using(var ctx = new OrganizationServiceContext(ctx))
{
var x = from q in ctx.CreateQuery<Quote>()
where q.QuoteId ! = currentQuote.Id
where q.OpportunityId != null
where q.cgk_quotetypecode != null
where q.cgk_quotetypecode == QuoteTypeCode.Regular || QuoteTypeCode.ServiceUnderWarranty
select new Quote
{
Id = q.Id,
cgk_totalnetprice = x.cgk_totalnetprice
};
var quotes = x.ToList();
}

Related

Is it possible to combine multiple IQueryable into a single one without using Union?

Good evening. Is it possible to combine multiple IQueryable into a single one without using Union in EF Core 2? The main problem is that it doesn't allow to use methods like .Union(), .Except() etc.
It's important that IQueryable should be executed as a single query.
This is what I want to get: toCreateSubquery.Union(toDeleteSubquery)
The queries I want to combine are listed below.
var toCreateSubquery =
validLinks
.Where(
rl => !existingLinks.Any(
el => el.Contract == rl.Contract && el.Estate == rl.Estate)) // Except() doesn't work'
.Select(x => new {x.Contract, x.Estate, Type = ActionType.Create});
var toDeleteSubquery =
existingLinks
.Where(el => !validLinks.Any(rl => el.Contract == rl.Contract && el.Estate == rl.Estate))
.Select(x => new {x.Contract, x.Estate, Type = ActionType.Delete});
This is the visualization of the problem I'm solving.
I want to get the union of these sets without intersection and be able to distinguish belonging to one of these sets.
Just in case, I attach the code of getting these sets:
var validLinks = from ac in _context.AreaContracts
from e in _context.Estate
where (from al in e.AreaLinks select al.Area.Id).Contains(ac.Area.Id) ||
e.Geometry.Intersects(
ac.Area.Geometry)
select new { Contract = ac.Contract.Id, Estate = e.Id };
var existingLinks = from sd in _context.SquareDistributions
select new { Contract = sd.Contract.Id, Estate = sd.Estate.Id };
Thank you for your attention.

Linq query (could not be translated)

I have very simple Linq query that shows error on run time
the query is
// string[] Blocks = _TableView.permission.BlockList.Split(',');
// string[] Permissions = _TableView.permission.PermissionsList.Split(',');
string[] Blocks = ... some string array ...;
string[] Permissions = ... some string array ... ;
var test = await (from t in _context.DocumentIn
where
Permissions.Contains(t.Imp_Exp.ToString()) &&
Permissions.Contains(t.confLevel.ToString()) &&
Blocks.Contains(t.BlockName)
select t).ToListAsync();
the error is
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 either AsEnumerable(),
AsAsyncEnumerable(), ToList(), or ToListAsync().
some notes
when I trace I find Blocks and Permissions full of values.
when I remove from the query two conditions of Permissions it works like the following
var test = await (from t in _context.DocumentIn
where
/*Permissions.Contains(t.Imp_Exp.ToString()) &&
Permissions.Contains(t.confLevel.ToString()) &&*/
Blocks.Contains(t.BlockName)
select t).ToListAsync();
In case Permissions is an array of string based on integers, an easy solution is to cast the array to int:
int[] Permissions = _TableView.permission
.PermissionsList
.Split(',')
.Select(s => int.Parse(s)).ToArray();
And remove the ToString call which are that are causing the exception:
var test = await (from t in _context.DocumentIn
where Permissions.Contains(t.Imp_Exp) &&
Permissions.Contains(t.confLevel) &&
Blocks.Contains(t.BlockName)
select t).ToListAsync();
But the main idea is that, removing the ToString call, whatever is your scenario, you should try to remove them from your query

Entity Framework 6: Disable Lazy Loading and specifically load included tables

Our current system is using Lazyloading by default (it is something I am going to be disabling but it can't be done right now)
For this basic query I want to return two tables, CustomerNote and Note.
This is my query
using (var newContext = new Entities(true))
{
newContext.Configuration.LazyLoadingEnabled = false;
var result = from customerNotes in newContext.CustomerNotes.Include(d=>d.Note)
join note in newContext.Notes
on customerNotes.NoteId equals note.Id
where customerNotes.CustomerId == customerId
select customerNotes;
return result.ToList();
}
My result however only contains the data in the CustomerNote table
The linked entities Customer and Note are both null, what am I doing wrong here?
I got it working with the following which is much simpler than what I've found elsewhere
Context.Configuration.LazyLoadingEnabled = false;
var result = Context.CustomerNotes.Where<CustomerNote>(d => d.CustomerId == customerId)
.Include(d=>d.Note)
.Include(d=>d.Note.User);
return result.ToList();
This returns my CustomerNote table, related Notes and related Users from the Notes.
That is callled eager loading you want to achieve.
var customerNotes = newContext.CustomerNotes.Include(t=> t.Node).ToList();
This should work, i don't really understand the keyword syntax.
If the code above doesn't work try this:
var customerNotes = newContext.CustomerNotes.Include(t=> t.Node).Select(t=> new {
Node = t.Node,
Item = t
}).ToList();

How to obtain a subset of records within a context using EntityFramework?

A newbie question. I am using EntityFramework 4.0. The backend database has a function that will return a subset of records based on time.
Example of working code is:
var query = from rx in context.GetRxByDate(tencounter,groupid)
select rx;
var result = context.CreateDetachedCopy(query.ToList());
return result;
I need to verify that a record does not exist in the database before inserting a new record. Before performing the "Any" filter, I would like to populate the context.Rxes with a subset of the larger backend database using the above "GetRxByDate()" function.
I do not know how to populate "Rxes" before performing any further filtering since Rxes is defined as
IQueryable<Rx> Rxes
and does not allow "Rxes =.. ". Here is what I have so far:
using (var context = new EnityFramework())
{
if (!context.Rxes.Any(c => c.Cform == rx.Cform ))
{
// Insert new record
Rx r = new Rx();
r.Trx = realtime;
context.Add(r);
context.SaveChanges();
}
}
I am fully prepared to kick myself since I am sure the answer is simple.
All help is appreciated. Thanks.
Edit:
If I do it this way, "Any" seems to return the opposite results of what is expected:
var g = context.GetRxByDate(tencounter, groupid).ToList();
if( g.Any(c => c.Cform == rx.Cform ) {....}

MongoDB - combining multiple Numeric Range queries (C# driver)

*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))