.Net 6: Linq enhancements

Linq has got some noticeably enhancements in the .Net 6. In this post, we will briefly visit some of them.

Specify Default Value for FirstOrDefault, LastOrDefault, SingleOrDefault

One of the features I am so relieved to see if the support for specifying default value for FirstOrDefault()LastOrDefault(), and SingleOrDefault(). I never quite understood why it wasn’t included in the first place. Previously the mentioned methods would return default(T) if the source return empty. With .Net 6, you could specify the default value which needs to be returned instead of default(t). For example

var data = GetData(10);

// Prior to .Net 6
var firstOrDefault = data.Where(x => x.Id > 100).DefaultIfEmpty(new User(-1, "GhostUser")).First();
var lastOrDefault = data.Where(x => x.Id > 100).DefaultIfEmpty(new User(-1, "GhostUser")).Last();
var singleOrDefault = data.Where(x => x.Id > 100).DefaultIfEmpty(new User(-1, "GhostUser")).Single();


// With .Net
var firstOrDefault = data.FirstOrDefault(x => x.Id > 100, new User(-1, "GhostUser"));
var lastOrDefault = data.LastOrDefault(x => x.Id > 100, new User(-1, "GhostUser"));
var singleOrDefault = data.SingleOrDefault(x => x.Id > 100, new User(-1, "GhostUser"));

The improvement is quite easy to see. The code has become more cleaner and verbose.

KeySelector for ExceptBy/ IntersectBy/ DistinctBy/ UnionBy/ MaxBy/ MinBy

Previously, if you needed to get the Max using a property using Linq methods, it was painful to say the least

var max = data.OrderByDescending(x => x.Id).First();

Things get even more ugly when we attempt Except(),Intersect(),Distinct() and Union() by a property. MoreLinq library had few methods which helped us in this regard, but with the inbuild Linq methods, this was still unavailable, until .Net 6.

The methods Except,Intersect, Distinct, Union, Max, and Min have got a variant which would now receive a key selector (Func<T,TKey>). Here is a short sample

demoCollection.ExceptBy(secondCollection.Select(x=>x.Name),user=>user.Name);
demoCollection.IntersectBy(secondCollection.Select(x => x.Name), user => user.Name);
demoCollection.DistinctBy(user=>user.Name);
demoCollection.UnionBy(secondCollection,user=>user.Name).;
demoCollection.MaxBy(user=>user.Id);
demoCollection.MinBy(user=>user.Id);

Range for Take

Range was introduced in C# 8. .Net 6 now adds an override to the Take() method which would accept a Range. For example,

demoCollection.Take(10..^10);

Chunk

Third Party Linq add-on libraries had this functionality for a long time now, but Microsoft decided to add chunk to its own set of extension methods of IEnumerable with .Net 6. Chunk allows you to process large collection of data in batches. For example, the following method call would split the demoCollection into chunks of 5, so that you could process them as batches.

demoCollection.Chunk(5);

Zip with IEnumerables

Linq methods previously allowed developers to merge an element in the first collection with an element in the second collection using the Zip method.Previously, if you need to Zip 3 collection, you would need to opt for a solution that might look like following.

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };
string[] romans = { "I", "II", "III" };

var numbersAndWords = numbers.Zip(words, (number, word) => (number, word));
var numbersAndWordsAndRoman = numbersAndWords.Zip(romans,(numberWord,roman)=> (numberWord.number, numberWord.word,roman));

foreach (var (number, word, roman) in numbersAndWordsAndRoman)
    WriteLine($"{number}-{word}-{roman}");

The new improvements now allows you to include 3 collections in your operation.

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };
string[] romans = { "I", "II", "III" };

var numbersAndWords = numbers.Zip(words,romans);

foreach (var (number,word,roman) in numbersAndWords)
    WriteLine($"{number}-{word}-{roman}");

That definitely has got a lot more cleaner.

TryGetNonEnumeratedCount

There are times when you would like to declare explicitly the capacity of the collection. This is quite useful when you do not want the capacity to double each time count exceeds capacity (not to mention all the copy operations involved). The TryGetNonEnumeratedCount method attempts to obtain the count of the source enumerable without forcing an enumeration. If it doesn’t find a property (say Count from ICollection) then it would return 0;

demoCollection.TryGetNonEnumeratedCount(out var enumCount); 

Those are some of the improvements in .Net 6 for Linq. We will continue exploring the .Net 6 features in the blogs to come. For the complete source of the code in the post, please use my Github.

Linq Join on Like%

One of the recent requirements I came across recently needed what looks like a Join on a ‘Like%’. Let me cite an example to demonstrate the requirement.
Consider the Collections below.

Master Collection

ID Name
1 Jia Anu
2 Sreena Anu
3 Anu Viswan

Child Collection

ID First Name Age
1 Jia 2
3 Sreena 34
5 Anu 35

I would like to Join on Master.Name and Child.FirstName where Child.FirstName StartsWith Master.Name. The expected output is as follows.

Name FirstName Age
Jia Anu Jia 2
Sreena Anu Sreena 34
Anu Viswan Anu 35

Let’s begin by defining the input collections.

var master = new List<Master>
{
new Master{Id=1,Name="Jia Anu"},
new Master{Id=2,Name="Sreena Anu"},
new Master{Id=3,Name="Anu Viswan"}
};

var child = new List<Child>
{
new Child{Id=1,FirstName="Jia",Age=2},
new Child{Id=2,FirstName="Sreena",Age=34},
new Child{Id=3,FirstName="Anu",Age=35}
};

Where Master and Child is defined as

public class Master
{
public int Id {get;set;}
public string Name {get;set;}
}

public class Child
{
public int Id{get;set;}
public string FirstName {get;set;}
public int Age {get;set;}
}

The default Join uses Equality as Comparer, and is not useful for us in this scenario. The solution lies in a little known overload of Group, which allows to provide our own customer Comparer. Let’s define our Custom Comparer.

class StartsWithEqualityComparer: IEqualityComparer<string>
{
public bool Equals(string right, string left)
{
return left.StartsWith(right);
}
public int GetHashCode(string obj) { return 0; }
}

Now, we can use our StartsWithEqualityComparer.

var result = master.Join(child,
m=>m.Name,
c=>c.FirstName,
(m,c)=> new
{
Name = m.Name,FirstName = c.FirstName,Age = c.Age
}
},new StartsWithEqualityComparer());

That’s it, we have our GroupBy with Like%.

Linq Recipes : IsIncreasing/IsDecreasing & IsAlternating

LINQ, ever since its introduction has turned out to be one of the favorite tools for .Net Developers. The post focus on couple of quick recipes for common scenarios.

Scenario 001
You have a collection of objects and you would like verify if the collection is sorted in increasing/decreasing order.

Recipe

We could always sort the collection and compare with the original version, however, if you really want to stick to a LINQ based solution, following extension methods is one approach.

public static bool IsIncreasing<TSource>(this IEnumerable<TSource> data) 
{ 
   return data.Zip(data.Skip(1),(first, second) => Comparer.Default.Compare(first,second) < 0).All(b => b);
}


public static bool IsDecreasing<TSource>(this IEnumerable<TSource> data)
{
   return data.Zip(data.Skip(1),(first, second) => Comparer.Default.Compare(first,second) > 0).All(b => b);
}

Demo

void Main()
{
       var itemSet1 = new[] {1,3,4,5,6};
       var itemSet2 = new[] {5,4,3,2,1};
       Console.WriteLine($"Is {nameof(itemSet1)} increasing ? {itemSet1.IsIncreasing()}" );
       Console.WriteLine($"Is {nameof(itemSet1)} decreasing ? {itemSet1.IsDecreasing()}" );
       Console.WriteLine($"Is {nameof(itemSet2)} increasing ? {itemSet1.IsIncreasing()}" );
       Console.WriteLine($"Is {nameof(itemSet2)} decreasing ? {itemSet1.IsDecreasing()}" );
}

Output

Is itemSet1 increasing ? True
Is itemSet1 decreasing ? False
Is itemSet2 increasing ? True
Is itemSet2 decreasing ? False

Scenario 002:

Consider you have a Collection ‘A’. You want to ensure that each element in the series vary in an alternating way, that is, for each element in A, A[i] < A[i+1] > A[i+2] or A[i] > A[i+1] < A[+2]. For example, for the collection {1,3,2,4,3,5} follows the pattern.

Recipe
Once again we will write the solution as an extension method. Well, that’s easier to ‘reuse’ right.

public static bool IsAlternating<TSource>(this IEnumerable<TSource> dataList)
{
       return VerifyIfAlternating(dataList.ToList(),true) || VerifyIfAlternating(dataList.ToList(),false);
}

private static bool VerifyIfAlternating<TSource>(IList<TSource> data,bool toggle)
{
return data.Zip(data.Skip(1), (first, second) =>
                                            {
                                                toggle = !toggle;
                                                return Comparer.Default.Compare(first,second) > 0 == toggle;
                                            })
                                            .All(b=>b);
                                                              
}

Demo

void Main()
{
       var itemSet3 = new[] {1,4,1,4,1};
       var itemSet4 = new[] {2,4,3,2,1};

       Console.WriteLine($"Is {nameof(itemSet3)} alternating ? {itemSet3.IsAlternating()}" );
       Console.WriteLine($"Is {nameof(itemSet4)} alternating ? {itemSet4.IsAlternating()}" );

}

Output

Is itemSet3 alternating ? True
Is itemSet4 alternating ? False

That’s it for now. This is one series I would like to continue.

Linq : Significance of AsEnumerable

While LINQ provides a big umbrella to query in-memory as well as remote collections alike (part of the benefit is having a standard vocabulary to query any collection), there are subtle differences, which rises from the way the two classes of Linq queries work.

A Linq query over in-memory collections usually work over the IEnumerable extensions, while the queries over LINQ to SQL work over the IQueryable extensions. This is a subtle difference, considering the IQueryable inherits from IEnumerable, but the consequences can strike you if you are not expecting them.

IQuerable approach (remote query) translates the entire query to the corresponding Database Query and execute it over in the Server.

However, this means one needs to be aware that this approach has its limitations set to the Database Server capabilities, for example, Linq To Sql, is to restricted to the capabilities of the concerned Database Server.

Consider the following example. The following code attempts to read data from Exam table, where the Description Field contains a Date string. For making our life easier, we are using a RegEx.
Regex regex = new Regex(@"([01]?[0-9]|2[0-3]):[0-5][0-9]");
var output = Exams.Take (100).Where(x=> regex.IsMatch(x.Description));

However, this would result in an error as Sql doesn’t recognize regular expression, which means, Linq wouldn’t be able to convert the statement to an equivalent Sql Query.

This is where the IEnumerable.AsEnumerable method comes into picture. Let’s examine the source of the extension method in Referencesource.

  public static IEnumerable<TSource> AsEnumerable<TSource>(this IEnumerable<TSource> source)
{
   return source;
}

The method as such, if you were to look up in reference source, might be doing very less, but its significance in the scheme of things is no less important. What it does is that it converts an IQueryable to IEnumerable and there by splits the Linq statement into 2, one that executes on the Server and other, as local collection.

For example, to fix our code above, we would need to change the statement as following.

var output = Exams.Take (100).AsEnumerable().Where(x=> regex.IsMatch(x.Description));

This would split our query into two. The first part would query the Database to fetch 100 rows from Exam table. The second part would then query the resulting collection locally using the Regex expression.

Although the differences are pretty evident once you use it, it is pretty useful to visit the backing classes to understand why it behaves so.