15

I need to check if all definitions contains some specific data. It works fine except the case when GroupBy returns empty collection.

var exist = dbContext.Definitions
                     .Where(x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
                     .GroupBy(x => x.PropertyTypeId)
                     .All(...some condition...);

How to rewrite this so All would return false on empty collection?

UPDATE: It is a LINQ to SQL and I wanted to execute this in single call.

UPDATE2: I think this works:

var exist = dbContext.Definitions
                     .Where(x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
                     .GroupBy(x => x.PropertyTypeId)
                     .Count(x => x
                        .All(...some condition...)) == propertyTypeIds.Count;
jlp
  • 9,800
  • 16
  • 53
  • 74

8 Answers8

13

If you're using LINQ to Objects, I'd just write my own extension method. My Edulinq project has sample code for All, and adapting that is pretty simple:

public static bool AnyAndAll<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    if (source == null)
    {
        throw new ArgumentNullException(nameof(source));
    }
    if (predicate == null)
    {
        throw new ArgumentNullException(nameof(predicate));
    }

    bool any = false;
    foreach (TSource item in source)
    {
        any = true;
        if (!predicate(item))
        {
            return false;
        }
    }
    return any;
}

This avoids evaluating the input more than once.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • I was about to write the same code just now but I would have probably kept a counter internally instead of a bool for added value. Not sure if I'd return it straight (as in `int AllAndCount(...)`) or in addition to the bool using an out parameter (as in ``bool AnyAndAll(..., out int count)`). – dnickless Oct 24 '18 at 07:01
  • 1
    Also, kindly note that you can avoid the cost of setting `any = true` over and over again by using the following code at the expense of a certain amount of readability: `using (var enumerator = source.GetEnumerator()) { if (enumerator.MoveNext()) { do { if (!predicate(enumerator.Current)) { return false; } } while (enumerator.MoveNext()); return true; } return false; }` – dnickless Oct 24 '18 at 07:01
5

You may be able to do this using an Aggregate, along the lines of:

.Aggregate(new {exists = 0, matches = 0}, (a, g) =>
        new {exists = a.exists + 1, matches = a.matches + g > 10 ? 1 : 0})

(Here, g > 10 is my test)

And then simple logic that exists is greater than zero and that exists and matches have the same value.

This avoids running the whole query twice.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
5

You could make use of DefaultIfEmpty extension method, and adjust your some condition so that it evaluates null to false.

var exist = definitions
    .Where(x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
    .GroupBy(x => x.PropertyTypeId)
    .DefaultIfEmpty()
    .All(...some condition...));
nawfal
  • 70,104
  • 56
  • 326
  • 368
  • 1
    Very Clever! In other words, `.All(g => g != null && ( ...some condition... ))`. Extra parenthesis may not be required depending on how `...some condition...` is written. – devgeezer Oct 05 '16 at 15:41
1

Well, you can do it in two steps :

var definitions = definitions.Where(
                    x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
                     .GroupBy(x => x.PropertyTypeId);

var exist = definitions.Any() && definitions.All(...some condition...);
Fabjan
  • 13,506
  • 4
  • 25
  • 52
1

Edit: first answer wouldn't have worked.

If you rearrange your query somewhat, you can use DefaultIfEmpty without needing to change your condition:

var exist = dbContext.Definitions
                     .Where(x => propertyTypeIds.Contains(x.PropertyTypeId) 
                                  && x.CountryId == countryId)
                     .GroupBy(x => x.PropertyTypeId);

           // apply the condition to all entries, 
           // resulting in sequence of bools (or empty), 
           // to permit the next step
                     .Select(...some condition...) 

           //if seq is empty, add `false`
                     .DefaultIfEmpty(false)

           //All with identity function to apply the query and calculate result
                     .All(b => b)
         );
RoadieRich
  • 6,330
  • 3
  • 35
  • 52
  • 1
    To steal a comment form a deleted answer (that you can't see): "But then you need to negate the result from Any to get the same results as were being obtained from All (true if it's true for all, false otherwise) and you're right back at the same situation" – DavidG Aug 19 '16 at 13:16
  • @DavidG is the new version better? – RoadieRich Aug 19 '16 at 13:37
1

Here is another trick:

var exist = dbContext.Definitions
    .Where(x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
    .GroupBy(x => x.PropertyTypeId)
    .Min(some_condition ? (int?)1 : 0) == 1;

It utilizes the fact that the above Min<int?> method returns:

(A) null when the set is empty
(B) 0 if the condition is not satisfied for some element
(C) 1 if the condition is satisfied for all elements

so we simple check the result for (C) using the nullable value comparison rules.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
0

What about writing your own extension method? (I am pretty sure you will name it better)

public static bool NotEmptyAll<T>(
    this IEnumerable<T> collection, 
    Func<T, bool> predicate)
{
    return collection != null
        && collection.Any()
        && collection.All(predicate);
}

Then call it instead of All

var exist = definitions.Where(
        x => propertyTypeIds.Contains(x.PropertyTypeId) && x.CountryId == countryId)
         .GroupBy(x => x.PropertyTypeId)
         .NotEmptyAll(
             ...some condition...));
Adil Mammadov
  • 8,476
  • 4
  • 31
  • 59
  • 5
    Note that this executes the query twice, which may well be a bad idea. – Jon Skeet Aug 19 '16 at 12:57
  • Thanks for warning @JonSkeet. But it the only solution which comes into MY mind. As it is not tagged `EntityFramework`, I think GENERALLY it should not cost much. – Adil Mammadov Aug 19 '16 at 13:01
  • Well it'll cost twice as much as doing it once... it's also quite unLINQ-like... there's no regular LINQ operator which evaluates its input more than once, IIRC. – Jon Skeet Aug 19 '16 at 13:06
0

Here's the alternative to All that returns false if collection is empty:

var collection = Enumerable.Range(0, 0); //empty collection

collection
  .Select(IsValid)
  .DefaultIfEmpty(false)
  .All(b => b);

Or as an extension method:

public static bool AnyAndAll<T>(IEnumerable<T> collection, Func<T, bool> predicate) =>
  collection
    .Select(predicate)
    .DefaultIfEmpty(false)
    .All(b => b);
Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632