Can this expression be simplified? - c#

I am running code from a Room object to get all the IRoomDwellers of a specific type into a list. All IRoomDwellers are stored in a static Dictionary<IRoomDweller,Room>.
However, this is basically my first time using Linq, and I feel (though the code appears to work) that the expression is rather unwieldy. For one, I don't really get what value the GroupBy is adding, but it does not appear to work without it.
The method is as follows:
private List<T> GetDwellers<T>() where T : IRoomDweller {
return RoomDwellers
.GroupBy(z => z.Value)
.SelectMany(z => z)
.Where(z => z.Value == this && z.Key is T)
.Select(z => (T) z.Key)
.ToList();
}
What function does the GroupBy actually serve (I cribbed that from an example when my initial attempt didn't work)? And can this expression otherwise be simplified/made to be more performant?

It's not ideal to have a dictionary (which is intended to be used to get a Room, if you have a RoomDweller) and use it the wrong way round (you have a Room and want to get a RoomDweller) but:
return RoomDwellers.Where(rdkvp => rdkvp.Value == this && rdkvp.Key is T)
.Select(rdkvp => rdkvp.Key)
.ToList();
RoomDwellers is a list of KeyValuePair, Key is a RoomDweller, Value is a Room.. So you can enumerate the list of KVPs, choosing those where the Value is this room, and then return the associated Key (the RoomDweller)

Related

LINQ where clause, filter on boolean(grandchild) inside array(child) that's inside array(parent)

If this is a duplicate please let me know, I searched everywhere.. This may just be a simple LINQ concept that I am not familiar with.
This is a simplified version of my data structure:
public interface ICourse
{
List<ISession> Sessions { get; set; }
}
public interface ISession
{
Boolean InRange { get; set; }
}
Problem: I have a List<ICourse>. I want to return all courses (ICourse), but filter the sessions (ISession) on those courses and only include sessions that are InRange (true).
Tried:
List<ICourse> results = //data retrieval.
return results.Where(course => course.Sessions
.Where(session => session.InRange).ToList<ISession>())
.ToList<ICourse>();
Errors:
Cannot implicitly convert type 'System.Collections.Generic.List<ISession>' to 'bool'.
Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type.
Obviously it does not complain when I only use one Where clause, i.e.:
return results.Where(course => course.Sessions.Count > 0).ToList<ICourse>();
results
.Where(course =>
course.Sessions.Where(session => session.InRange).ToList<ISession>()
)
.ToList<ICourse>();
Where() is a filter. It needs a lambda that returns bool. Where gives your lambda each item in the enumeration in turn, and the lambda is expected to answer a yes-or-no question: "Do you want to include this thing in the results?"
Here's the answer you're trying to give to that question:
course.Sessions.Where(session => session.InRange).ToList<ISession>()
That returns a list of ISession. Where asks your lambda, "Do you want this thing?" and instead of "yes" or "no", your lambda says, "Hey, look at all these sessions!" In C#, that's not a yes-or-no answer.
The compiler sees this comedy routine coming a mile away, and pulls the plug before anything silly happens at runtime.
So.
If you want to return a list of ICourse, but you want them to have only a subset of the sessions they start with, you can't do that without creating new course objects. Since what you've got here are interfaces, let's not try to create new objects. I doubt you'd want to in any case.
You can get a flat list of all sessions that are InRange:
results.SelectMany(c => c.Sessions.Where(s => s.InRange)).ToList();
And you can get a list of tuples or an anonymous type which pair a reference to an ICourse with a list of in-range sessions belonging to that course:
results.Select(c =>
new Tuple<ICourse, List<ISession>>(
c,
c.Sessions.Where(session => session.InRange).ToList()
))
.ToList();
And you can filter that to exclude any courses that have no in-range sessions:
results.Select(c =>
new Tuple<ICourse, List<ISession>>(
c,
c.Sessions.Where(session => session.InRange).ToList()
))
.Where(t => t.Item2.Any())
.ToList();
If this isn't just an ad-hoc thing off in some little corner of your application, I'd urge you to write a custom class to replace that Tuple.
Or in C#7:
public List<(ICourse Course, List<ISession> Sessions)>
GetInRangeSessions(IEnumerable<ICourse> courses)
=>
courses
.Select(c =>
(Course: c, Sessions: c.Sessions.Where(session => session.InRange).ToList())
)
.ToList();
Just a few more irritating superfluous parentheses in this language, and we'll be mining the asteroids.
Ed Plunkett's answer is correct, but I must ask, why do you want a new ICourse objects list. Why not just use the full ICourse list, and just filter the Sessions where they are being used:
foreach(var c in Courses)
{
foreach(var s in c.Sessions.Where(session => session.InRange))
{.... }
}
I think the simplest idea is just to write:
List<Sessions> temporary = new List<Sessions>();
foreach (Course s in courses)
{
temporary = s.sessions.Where(x => x.InRange == true).ToList();
s.sessions = temporary;
temporary.Clear();
}
If you want all Course Where a Session is InRange, I would try... (Not tested)
List<ICourse> results = //data retrieval.
return results.Where(course => course.Sessions.Where(session => session.InRange)).ToList<ISession>()).ToList<ICourse>();

EF: how to reuse same filter in both standard query and selectMany

I am new to EF and LINQ.
The following two pieces of code work:
dbContext.Categories.Where(cat => [...big ugly code for filtering...] );
&
dbContext.Products.Where(prod => prod.PROD_UID == 1234)
.SelectMany(prod => prod.Categories.Where(
cat => [...big ugly code for filtering...] );
But I want somehow to create only one, reusable, expression or delegate for my filter. I have the following:
private static Expression<Func<Category, bool>> Filter(filter)
{
return cat => [...big ugly code for filtering...] ;
}
but I cannot use it in SelectMany.
I am aware that:
Where clause of standard query accepts Expression<Func<Category,bool>> and returns IQueryable<Category>
Where clause of SelectMany accepts Func<Category,bool> and returns IEnumerable<Category>.
What is the best way to accomplish this? Are any tricks here?
PS: I want in the end to get all categories of a product.
It looks like you're trying to use SelectMany as a filter. SelectMany is used to flatten a collection of collections (or a collection of a type that contains another collection) into one flat collection.
I think what you want is:
dbContext.Products.Where(prod => prod.PROD_UID == 1234)
.SelectMany(prod => prod.Categories)
.Where(filter);
In which case you can reuse the same expression to filter.
EDIT
Based on your updated question it looks like you are applying Where to an IEnumerable<T> property, so the compiler is binding to IEnumerable.Where which takes a Func instead of an Expression.
You should be able to just call AsQueryable() on your collection property to bind to IQueryable.Where():
dbContext.Products.Where(prod => prod.PROD_UID == 1234)
.SelectMany(prod => prod.Categories
.AsQueryable()
.Where(filter);
The next option would be to compile the expression to turn it into a Func:
dbContext.Products.Where(prod => prod.PROD_UID == 1234)
.SelectMany(prod => prod.Categories
.Where(filter.Compile());
But it wouldn't surprise me if the underlying data provider isn't able to translate that to SQL.
All you need to do is call the Filter function before executing the query and store it in a local variable. When the query provider sees a method it attempts to translate that method into SQL, rather than executing the method and using the result. Whenever it encounters a local variable it doesn't attempt to translate it into SQL but rather evaluates the variable to its value, and then uses that value in the query.
As for the problems that you're having due to the fact that the relationship collection isn't an IQueryable, it's probably best to simply approach the query differently and just pull directly from the categories list instead:
var filter = Filter();
dbContext.Categories.Where(filter)
.Where(cat => cat.Product.PROD_UID == 1234);
After analyzing in more detail the SQL generated by EF, I realized that having the filter inside SelectMany is not more efficient.
So, both suggestions (initial one from DStanley and Servy's) should be ok for my case (many-to-many relation between Categories and Products)
/* 1 */
dbContext.Products.Where(prod => prod.PROD_UID == 1234)
.SelectMany(prod => prod.Categories)
.Where( Filter ); // reuseable expression
this will result into a INNER JOIN
/* 2 */
dbContext.Categories.Where( Filter ) // reuseable expression
.Where(c => c.Products.Where(prod.PROD_UID == 1234).Any());
this will result into a EXISTS (sub-select)
The execution plan seems to be absolutely identical for both in my case; so, I will choose for now #2, and will keep an eye on performance.

how to filter entity type framework object by its child object value properties?

I have an entity framework object called batch, this object has a 1 to many relationship to items.
so 1 batch has many items. and each item has many issues.
I want to filter the for batch items that have a certain issue code (x.code == issueNo).
I have written the following but Im getting this error:
items = batch.Select(b => b.Items
.Where(i => i.ItemOrganisations
.Select(o => o
.Issues.Select(x => x.Code == issueNo))));
Error 1:
Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<System.Collections.Generic.IEnumerable<bool>>' to 'bool'
Error 2:
Cannot convert lambda expression to delegate type 'System.Func<Ebiquity.Reputation.Neptune.Model.Item,bool>' because some of the return types in the block are not implicitly convertible to the delegate return type
Select extension method needs a lambda expression that returns a boolean, but the inner o.Issues.Select returns an IEnumerable of boolean to the outer Select(o => o which result in the exception you're getting.
Try using Any instead which verifies that at least one element verifies the condition:
items = batch.Select(
b => b.Items.Where(
i => i.ItemOrganisations.Any(
o => o.Issues.Any(x => x.Code == issueNo)
)
)
);
If I understand correctly, you're trying to select through multiple layers of enumerables. In those cases you need SelectMany which flattens out the layers, not Select. LINQ's syntax sugar is made specifically to make SelectMany easier to reason about:
var items = from item in batch.Items
from org in item.ItemOrganizations
from issue in org.Issues
where issue.Code == issueNo
select item;
The compiler translates that into something like this:
var items = batch.Items
.SelectMany(item => item.ItemOrganizations, (item, org) => new {item, org})
.SelectMany(#t => #t.org.Issues, (#t, issue) => new {#t, issue})
.Where(#t => #t.issue.Code == issueNo)
.Select(#t => #t.#t.item);
You can always wrap this in a Distinct if you need to avoid duplicate items:
var items = (from item in batch.Items
from org in item.ItemOrganizations
from issue in org.Issues
where issue.Code == issueNo
select item).Distinct();
It's hard to tell what you're trying to do based on your code but I think you're looking for something like this;
var issue = batch.Select(b => b.Items).Select(i => i.Issues).Where(x => x.Code == issueNo).Select(x => x).FirstOrDefault();
The above query will return the first issue where the Issues Code property is equal to issueNo. If no such issue exists it will return null.
One problem (the cause of your first error) in your query is that you're using select like it's a where clause at the end of your query. Select is used to project an argument, when you do Select(x => x.Code == issueNo) what you're doing is projecting x.Code to a bool, the value returned by that select is the result of x.Code == issueNo, it seems like you want that condition in a where clause and then you want to return the issue which satisfies it which is what my query is doing.
items = from b in batch.Include("Items")
where b.Items.Any(x=>x.Code==issueNo)
select b;
You're getting lost in lambdas. Your LINQ chains are all embedded in each other, making it harder to reason about. I'd recommend some helper functions here:
static bool HasIssueWithCode(this ItemOrganization org, int issueNo)
{
return org.Issues.Any(issue => issue.Code == issueNo);
}
static bool HasIssueWithCode(this Item items, int issueNo)
{
return items.ItemOrganizations.Any(org => org.HasIssueWithCode(issueNo));
}
Then your answer is simply and obviously
var items = batch.Items.Where(item => item.HasIssueWithCode(issueNo));
If you inline these functions, the result is the exact same as manji's (so give manji credit for the correct answer), but I think it's a bit easier to read.

Is there an opposite of LINQ's All method?

I'm currently using
a_list.All(item => !(item.field_is_true == true))
which works well, but I'd like to know if there was a proper LINQ method to do the opposite of all.
All() checks that a given Predicate returns true for all items. In terms of framework development, it wouldn't make any sense to write a seperate method that checks that a given Predicate returns false for all items, as it is so easy to "not" a predicate.
However, you can write your own extension method:
public static bool None<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return !source.Any(predicate);
}
The exact opposite of All() is essentially None, but since LINQ has no None() method, you can accomplish the same result through !set.Any().
!a_list.Any(item => item.matches == true)
This will produce true if none of the items in a_list have a matches value that is true.
Another example:
names.All(item => item.StartsWith("R"))
is true if all of the items in names start with R (as you know already).
!names.Any(item => item.StartsWith("R"))
is true if none of the items in names start with R.
Based on your comment below, it sounds like you might just be looking for a way to accomplish the same result as your current code snippet, but in a different way. This should provide the same result as your current code, but without the ! in the predicate:
!a_list.Any(item => item.matches == true)
This can be further simplified to:
!a_list.Any(item => item.matches)
I'd imagine yours could be simplified as well, to this:
a_list.All(item => !item.matches)
There's rarely a good reason to explicitly compare a boolean value with true or false.
you wrote:
a_list.All(item => !(item.field_is_true == true))
that is like doing:
a_list.All(item => item.flag== false) // more readable to me...
//return true if all of the items have a **flase** value on their flag
you can also use .any() to achieves the same result:
!a_list.Any(item => item.flag==true)
as for performence issues: .any() vs .all() - both would have identical performance
(when linq to object is used) , find more here : LINQ: Not Any vs All Don't
Rather than negate the All() condition, simply use the Any() with the same predicate and treat the returned boolean appropriately.
So, rather than:
bool conditionDoesntExist = a_list.All(item => !(item.field_is_true == true));
you can have
bool conditionDoesExist = a_list.Any(item => item.field_is_true == true)
Note the change in name of the flag. (Of course I'm overlooking semantic stuff like the original predicate could have been written as item => item.field_is_true == false or simply item => !item.field_is_true ).
If you want to keep the flag name the same then still use the Any() but negate it:
bool conditionDoesntExist = !a_list.Any(item => item.field_is_true == true);
All
a_list.All(item => item.condition)
Some
a_list.Any(item => item.condition)
Not all
!a_list.All(item => item.condition)
or:
a_list.Any(item => !(item.condition))
None
!a_list.Any(item => item.condition)
or
a_list.All(item => !(item.condition))

Return Modal Average in LINQ (Mode)

I am not sure if CopyMost is the correct term to use here, but it's the term my client used ("CopyMost Data Protocol"). Sounds like he wants the mode? I have a set of data:
Increment Value
.02 1
.04 1
.06 1
.08 2
.10 2
I need to return which Value occurs the most "CopyMost". In this case, the value is 1. Right now I had planned on writing an Extension Method for IEnumerable to do this for integer values. Is there something built into Linq that already does this easily? Or is it best for me to write an extension method that would look something like this
records.CopyMost(x => x.Value);
EDIT
Looks like I am looking for the modal average. I've provided an updated answer that allows for a tiebreaker condition. It's meant to be used like this, and is generic.
records.CopyMost(x => x.Value, x => x == 0);
In this case x.Value would be an int, and if the the count of 0s was the same as the counts of 1s and 3s, it would tiebreak on 0.
Well, here's one option:
var query = (from item in data
group 1 by item.Value into g
orderby g.Count() descending
select g.Key).First();
Basically we're using GroupBy to group by the value - but all we're interested in for each group is the size of the group and the key (which is the original value). We sort the groups by size, and take the first element (the one with the most elements).
Does that help?
Jon beat me to it, but the term you're looking for is Modal Average.
Edit:
If I'm right In thinking that it's modal average you need then the following should do the trick:
var i = (from t in data
group t by t.Value into aggr
orderby aggr.Count() descending
select aggr.Key).First();
This method has been updated several times in my code over the years. It's become a very important method, and is much different than it use to be. I wanted to provide the most up to date version in case anyone was looking to add CopyMost or a Modal Average as a linq extension.
One thing I did not think I would need was a tiebreaker of some sort. I have now overloaded the method to include a tiebreaker.
public static K CopyMost<T, K>(this IEnumerable<T> records, Func<T, K> propertySelector, Func<K, bool> tieBreaker)
{
var grouped = records.GroupBy(x => propertySelector(x)).Select(x => new { Group = x, Count = x.Count() });
var maxCount = grouped.Max(x => x.Count);
var subGroup = grouped.Where(x => x.Count == maxCount);
if (subGroup.Count() == 1)
return subGroup.Single().Group.Key;
else
return subGroup.Where(x => tieBreaker(x.Group.Key)).Single().Group.Key;
}
The above assumes the user enters a legitimate tiebreaker condition. You may want to check and see if the tiebreaker returns a valid value, and if not, throw an exception. And here's my normal method.
public static K CopyMost<T, K>(this IEnumerable<T> records, Func<T, K> propertySelector)
{
return records.GroupBy(x => propertySelector(x)).OrderByDescending(x => x.Count()).Select(x => x.Key).First();
}

Categories