C# Linq Include() with where() - c#

Have a query that looks something like this
var myResult = await _context.Families.AsNoTracking().AsSplitQuery()
.Include(f => f.Dogs.Where(d => d.Name == "someName"))
.Where(f => f.Breed == "some breed")
.Where(f => f.Dogs.Count > 0)
.ToListAsync(cancellationToken);
Have used dummy data*
In my query I'm finding that the include statement works as expected however the last .Where() is not. I intent to filter all 'families' where they have a dog with name == "someName" and breed == "some breed". The main point being, I only want to return dogs in the dog list where the name == "someName"
I'm finding that that last Where() clause is doing a .count on the entire list of dogs and not the filtered included dogs we want in the response.
Can of course replicate the f.Dogs.Where(mp => mp.Name == "someName") inside the last .Where() but is there no way to do it without repeating the logic.
Split Query is required. Query is much larger with multiple includes, but shortened it for post
What am I missing

Given that you have shorted your query for the post, what I am about to say may or may not apply.
Warning Include only works if the there is a database relationship between Families and Dogs (which I assume there is).
This query could be simplified so that you are not running multiple queries on the database. Will this query help you?
var myResult = await _context.Families
.Join(Dogs, fam => fam.Id, dog => dog.FamilyId,
(fam, dog) => new { fam, dog })
.Join(Breeds, a => a.dog.BreedId, breed => breed.Id,
(a, breed) => new {
Family = a.fam,
Dog = a.Dog,
Breed = breed
})
.Where(m => m.Dog.Name == "someName"
&& m => Breed.Name == "some breed")
.AsNoTracking()
.ToListAsync(cancellationToken)
.GroupBy(g => g.Family)
.Where(r => r.Count() > 0);
This will allow you to query without putting sub-selects in your where clause which are very inefficient. The ToList or ToListAsync will cause your query to run. You get your results, then you can filter out any families that do not have at least one dog. Granted I may have assumed something or missed something else not knowing your exact schema. If you want more help, let me know your basic schema and I can try to come up with something else.

Related

Does it matter where AsNoTracking in Entity Framework is called

Does it matter where the AsNoTracking method is called when writing an Entity Framework query? e.g.
var matchingCustomers = context.Customers.AsNoTracking().Where(n => n.city == "Milan").Skip(50).Take(100).OrderBy(n => n.Name).ToList();
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").AsNoTracking().Skip(50).Take(100).OrderBy(n => n.Name).ToList();
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).AsNoTracking().Take(100).OrderBy(n => n.Name).ToList();
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).Take(100).AsNoTracking().OrderBy(n => n.Name).ToList();
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).Take(100).OrderBy(n => n.Name).AsNoTracking().ToList();
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).Take(100).OrderBy(n => n.Name).ToList().AsNoTracking();
I like adding it to the end of statements but before the ToList is called like this:
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).Take(100).OrderBy(n => n.Name).AsNoTracking().ToList();
No it doesn't matter: (source)
A new query with NoTracking applied, or the source query if NoTracking is not supported.
So you either do it in the beginning and you expand the "new" query with the method chain, or you do it in the end and then get the "new" query. As long as you call it before the query is executed you're fine.
As Cdaragorn said in the comments.
You cannot call AsNoTracking after ToList because you no longer have
an IQueryable to call it on. It will give a compile time error.
In the case you could do what OP is asking I am going to explain how the query would work because could help others to understand these matters:
With
var matchingCustomers = context.Customers.Where(n => n.city == "Milan").Skip(50).Take(100).OrderBy(n => n.Name).ToList().AsNoTracking();
you are trying to apply NoTracking to a data structure already in memory once EF has executed, and traked, the query.
When you use this fluent API; you are defining a query without executing it until you, of course, execute the query. ToList() will execute the query an bring the data to memory to transform it into a List<T> data structure.
Let's split the command to understand this:
context.Customers --> Select [*] from Customers
Where(n => n.city == "Milan") --> Select [*] from Customers where city
== 'Milan'
Skip(50).Take(100) --> Select [*] from Customers where city == 'Milan'
OFFSET 50 ROWS FETCH NEXT 100 ROWS ONLY
OrderBy name --> Select [*] from Customers where city == 'Milan'
OFFSET 50 ROWS FETCH NEXT 100 ROWS ONLY ORDER BY name
ToList() --> Execute the query and bring the data into memory with Tracking by default!
AsNoTraking() --> Does nothing because EF already executed the query
and tracked the data.

Using .Where() clause inside an .Include() on a linq query with multiples includes

I would like to get a collection of Customers including several properties among which is the address but only when it has not been deleted yet (SuppressionDate == null)
IQueryable<Customer> customers =
context.Persons.OfType<Customer>()
.Include(customer => customer.Addresses)
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors);
I have tried several ways to use the where clause in order to filter address:
...
.Include(customer => customer.Addresses.Where(a => a.SuppressionDate == null))
.Include(customer => customer.Bills)
...
That was my first try but it raises the following exception:
System.ArgumentException : The Include path expression must refer to a
navigation property defined on the type. Use dotted paths for
reference navigation properties and the Select operator for collection
navigation properties. Parameter Name : path
I've also tried with the same where clause at the end of the Include() and at the end of the query but neither seems to work.
I'm currently using a workaround which is iterate through the collection of customer and remove the addresses that are deleted as such:
foreach(Customer c in customers){
customer.Addresses = customer.Addresses.Where(a => a.SuppressionDate == null).ToList();
}
Being fairly new to linq to object/entities, I was wondering if there was a built-in way to achieve this.
If you were getting a single customer you could use explicit loading like this:
var customer = context.Persons
.OfType<Customer>()
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors)
.FirstOrDefault(); //or whatever
context.Entry(customer).Collections(x => x.Addresses).Query().Where(x => x.SuppressionDate == null).Load();
That makes a nice query and two simple calls to the database. But in this case you are getting a list (or collection or whatever) of customer and there's no shortcuts. Your "workaround" may cause a lot of chatter with the database.
So you probably just have to take it one step at time:
//1. query db to get customers
var customers = context.Persons
.OfType<Customer>()
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors)
.ToList();
//2. make an array of all customerIds (no db interation here)
var customerIds = customers.Select(x => x.CustomerId).ToArray();
//3. query db to get addresses for customers above
var addresses = context.Addresses.Where(x => customerIds.Contains(x.CustomerId).ToList();
//4. assign addresses to customers (again, no db chatter)
foreach (var customer in customers)
{
customer.Addresses = addresses
.Where(x => x.CustomerId == customer.CustomerId && x.SuppressionDate == null)
.ToList();
}
Not too bad- still just two queries to database.

LINQ Query find columns where string contains any letter

I'm trying to find all customer codes where the customer has a status of "A" and whose code does not contain any letter using LINQ query.
var activeCustomers = Customers.Where(x => x.Status == "A" && x.Code.Any(n => !char.IsLetter(n))).Select(x => x.Code);
When I run this query in LinqPad I get the following error:
You'll need to do this as a two part query. First, you could get all the users who's status is "A":
var activeCustomers = Customers.Where(x => x.Status == "A").ToList();
After you've got those in-memory, you can create an additional filter for char.IsDigit:
var codes = activeCustomers.Where(x => x.Code.Any(n => !char.IsLetter(n)))
.Select(x => x.Code)
.ToArray();
As commenters have stated, IsLetter() cannot be translated to SQL. However, you could do the following, which will first retrieve all items with Status "A" from the database, then will apply your criteria after retrieval:
var activeCustomers = Customers.Where(x => x.Status == "A").AsEnumerable().Where(x => x.Code.Any(n => !char.IsLetter(n))).Select(x => x.Code);
You'll have to determine if it's acceptable (from a performance perspective) to retrieve all customers with "A" and then process.
The AsEnumerable() transitions your LINQ query to working not with IQueryable (which works with SQL) but with IEnumerable, which is used for plain LINQ to objects.
Since it is LINQ 2 SQL, there is no natural way to translate char.IsLetter to something SQL can understand. You can hydrate a query that retrieves your potential candidates and then apply an addition in-memory filter. This also solves the issue where LINQ 2 SQL has a preference for a string and you are dealing with chars
var activeCustomers = Customers.Where(x => x.Status == "A").ToList();
var filteredCustomers = activeCustomers.Where(x =>
x.Code.Any(n => !char.IsLetter(n))).Select(x => x.Code).ToList();
There are two performance hits here. First, you're retrieving all potential records, which isn't too desirable. Second, in your above code you were only interested in an enumerable collection of codes, which means our query is including far more data than we originally wanted.
You could tighten up the query by only returning back to columns necessary to apply your filtering:
var activeCustomers = Customers.Where(x => x.Status == "A")
Select(x => new Customer{ Status = x.Status, Code = x.Code }).ToList();
You still return more sets than you need, but your query includes fewer columns.

Why am I getting a Compile time error in this linq query?

I am getting a compile time error when compiling the below code and I can't see why:
Relations are many to many relations
var contacts = groups_to_querry
.SelectMany(x => x.Contacts)
.Where(x => x.ID == Guid.Empty)
.SelectMany(p => p.ContactDetails)
.Where(x => x.ID == Guid.Empty)
.SelectMany(x => x.Contacts); //This line gives me a compile time error
//Error : The Type argumetns for method 'System.Linq.Enumerable.SelectMany<TSource,Tresult>
//(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,
//System.Collections.Generic.IEnumerable<TResult>>)' cannot be infrred from the usage.
//Try specifying the type arguments explicitly
The second time you call for .SelectMany(x => x.Contacts), you are currently working with a collection of ContactDetails. It is doubtful that you would be able to use SelectMany on it. You would need to use Select instead.
SelectMany is used when you want to select multiple collections of items and put them into one IEnumerable. Select is used on individual fields. Since you are working with objects of type ContactDetail (which I assume can only have one contact), you would need use Select
EDIT: Here is what you're doing in a nutshell, step by step:
groups_to_querry.SelectMany(x => x.Contacts): From all the groups that I want to query select all of their many contacts. Each group has many contacts, so put them all into a single IEnumerable collection of type Contact
.Where(x => x.ID == Guid.Empty): ...but only those Contacts with an empty ID
.SelectMany(p => p.ContactDetails): Then select all of those Contacts' many ContactDetails. Each Contact has many ContactDetails, so put them all into a single IEnumerable collection of type ContactDetail
.Where(x => x.ID == Guid.Empty): ...but only those ContactDetails with an empty ID
.SelectMany(x => x.Contacts);: Now select each of the ContactDetails' many Contacts. However, since the compiler knows that there is a one-to-many relationship between Contacts and ContactDetails (and not the other way around) that statement is not possible, and thus shows a compile error
I'm interpreting your intended query as "from multiple groups of contacts, select all contacts that have ID=Guid.Empty and also have details that all have ID=Guid.Empty".
The way your code is actually interpreted is "from all contacts that have Guid.Empty, select all details that have Guid.Empty, and from those details select all contacts". The first problem is that you end up selecting from details. This means the final SelectMany should be a Select, because x.Contacts here refers to the many-to-one relationship from details to contacts.
The second problem is that the result will contain duplicates of contacts, because the same contact is included for each details. What you should be doing instead is filtering the contacts directly based on their details collections, like this:
groups_to_query
.SelectMany(g => g.Contacts)
.Where(c => c.ID == Guid.Empty)
.Where(c => c.ContactDetails.All(d => d.ID == Guid.Empty))
Note this would also select contacts that have zero details, which is different behavior from your query, so I'm not sure if it's what you want. You could add another filter for ContactDetails.Any() if not.
Edit: Since you're using Entity Framework, the above probably won't work. You may need to select the details in a subquery and then filter in memory after it executes:
var queryResult =
groups_to_query
.SelectMany(g => g.Contacts)
.Where(c => c.ID == Guid.Empty)
.Select(c => new {
contact = c,
detailIDs = c.ContactDetails.Select(d => d.ID)
}).ToList();
var contacts =
queryResult
.Where(r => r.detailIDs.All(id => id == Guid.Empty))
.Select(r => r.contact);

How to query (using LINQ) FormattedValues in Dynamics CRM 2011

I am using LINQ to retrieve Account type entities from Microsoft Dynamics CRM Online. I am unable to filter the list for a particular formatted value. I have the correct value, but am receiving zero records.
I'm creating my connection like this:
var connection = new CrmConnection("CRMOnline");
connection.ProxyTypesEnabled = true;
CrmOrganizationServiceContext _context = new CrmOrganizationServiceContext(connection);
I've tried:
List<Account> items = _context.CreateQuery<Account>()
.Where( c => ((OptionSetValue)c["new_accreditationstatus"]).Equals(7))
.ToList();
and
List<Account> items = _context.CreateQuery<Account>()
.Where( c => c.GetFormattedAttributeValue("new_accreditationstatus") == "7"
.ToList();
and
List<Account> items = _context.CreateQuery<Account>()
.Where( c => c["new_accreditationstatus"] == "7"
.ToList();
The last on throws a System.Format exception.
Filters on normal properties, i.e. .Where(c => c.AccountNumber.StartsWith("2010")) work perfectly fine.
You can only get access to the _____Set entities when generating the early-bound CRM file (look into crmsvcutil.exe/Xrm.cs online) and creating an early-bound derivative of CrmOrganizationServiceContext (commonly called XrmServiceContext). You can see the available constructors in the early-bound file.
So if you know the (int) value of the OptionSetValue in advance (7, in this case), you can just use this value as one of the arguments in the Where clause, as you've stated elsewhere:
.Where( c => c.new_AccreditationStatus.Value == 7)
EDIT (try this):
var list = _context.AccountSet.Where(c =>
c.FormattedValues["new_accreditationstatus"] == "7").ToList();
Another great question, but unfortunately, I think this will represent another failure/"limitation" of the Linq provider, which doesn't mention anything about FormattedValues as one of the permitted uses of the Where clause, though it is permitted as an item in the Select clause.
The actual values for OptionSetValues are stored in the StringMap entity, and incidentally enough, you can access the StringMap entity via Linq. An example is as follows.
// This query gets one permissible value for this entity and field.
var actualValue = _context.CreateQuery("stringmap")
.Where(x => x.GetAttributeValue<string>("attributename") == "new_accreditationstatus")
.Where(x => x.GetAttributeValue<int>("value") == "7")
.Where(x => x.GetAttributeValue<int>("objecttypecode") == Account.EntityTypeCode)
.Select(x => x.GetAttributeValue<string>("value"))
.Single();
However, trying to build on this with a subquery and a version of your original query, as in the below, results in an exception, also below.
var actualValues = _context.CreateQuery("stringmap")
.Where(x => x.GetAttributeValue<string>("attributename") == "new_accreditationstatus")
.Where(x => x.GetAttributeValue<int>("objecttypecode") == Xrm.Account.EntityTypeCode);
// This (modified) query uses the StringMap values from the previous query in
// a subquery, linking to the int (attributevalue) value that
// new_accreditationstatus represents.
List<Account> items = _context.CreateQuery<Account>()
.Where(c => actualValues
.Where(x => x.GetAttributeValue<int>("attributevalue") == c.new_accreditationstatus.Value)
.Select(x => x.GetAttributeValue<string>("attributevalue"))
.Single() == "7")
.ToList();
...throws an exception.
Privilege Type Read not defined on entity 'StringMap'.
Which is of course frustrating, because somehow, Linq allows you to query the string map in the first query.
So you'll have to first query the StringMap entity for the AttributeValue that corresponds to "7", then use that value in a new query that references that value as follows:
var actualValue = _context.CreateQuery("stringmap")
.Where(x => x.GetAttributeValue<string>("attributename") == "new_accreditationstatus")
.Where(x => x.GetAttributeValue<int>("value") == "7")
.Where(x => x.GetAttributeValue<int>("objecttypecode") == Account.EntityTypeCode)
.Select(x => x.GetAttributeValue<string>("attributevalue"))
.Single();
List<Account> items = _context.CreateQuery<Account>()
.Where(c => c.new_accreditationstatus = new OptionSetValue(actualValue)
.ToList();
If I can ever find a way to do all of this in one query, I will definitely edit and repost.
Have you seen the crmsvcutil extension that will generate enumerations for optionsets?

Categories