In this article, we’ll explore the capabilities of the MoreLINQ library, which enables us to improve the already robust LINQ functionality.
Let’s dive in!
What is LINQ to Objects?
LINQ to Objects is just another way of describing the use of LINQ queries. LINQ provides us with the means of querying and retrieving data from IEnumerable
or IEnumerable<T>
and any other collection in .NET that implements one of those interfaces.
Let’s create two classes to illustrate this. First, we create the Flight
class:
public class Flight { private readonly int MinNumberOfTickets = 20; private readonly int MaxNumberOfTickets = 50; public int FlightNumber { get; set; } public string DepartureCity { get; set; } public string ArrivalCity { get; set; } public DateTime DepartureTime { get; set; } public DateTime ArrivalTime { get; set; } public List<Ticket> Tickets { get; set; } public Flight( int flightNumber, string departureCity, string arrivalCity, DateTime departureTime, DateTime arrivalTime) { FlightNumber = flightNumber; DepartureCity = departureCity; ArrivalCity = arrivalCity; DepartureTime = departureTime; ArrivalTime = arrivalTime; Tickets = new List<Ticket>(); } public void AddTicket(Ticket ticket) { if (!AreThereFreeSeats()) { Console.WriteLine("No free seats on the plane!"); } else { Tickets.Add(ticket); } } public bool AreThereFreeSeats() { return Tickets.Count < MaxNumberOfTickets; } public bool WillFligthTakePlace() { return Tickets.Count >= MinNumberOfTickets && Tickets.Count <= MaxNumberOfTickets; } }
First, we create a Flight
class that holds basic information any flight might have – such as departure and arrival times and cities, flight numbers, and a list of tickets. It also has three simple methods that determine if the flight will take place and if there are any empty seats, as well as a method to add a Ticket
to the flight.
Next, we create the Ticket
class:
public class Ticket { public int TicketNumber { get; set; } public int SeatNumber { get; set; } public string PassengerName { get; set; } public string TicketClass { get; set; } public decimal Price { get; set; } public Ticket( int ticketNumber, int seatNumber, string passengerName, string ticketClass, decimal price) { TicketNumber = ticketNumber; SeatNumber = seatNumber; PassengerName = passengerName; TicketClass = ticketClass; Price = price; } }
Here we create a Ticket
class that has information about seat number, passenger name, class, and price.
LINQ to Object Method Examples
Now that we have things set up, we’ll start adding methods to our Flight
class using the built-in LINQ functionally.
Getting Min/Max Values
We add two methods, one for getting the cheapest Ticket
and another for getting the most expensive one:
public IEnumerable<Ticket> GetCheapestTickets() { var cheapestPrice = Tickets.Min(t => t.Price); return Tickets.Where(x => x.Price == cheapestPrice); } public IEnumerable<Ticket> GetMostExpensiveTickets() { var highestPrice = Tickets.Max(t => t.Price); return Tickets.Where(x => x.Price == highestPrice); }
We start by adding using System.Linq;
at the top of our file. Then we declare the GetCheapestTickets()
and GetMostExpensiveTickets()
methods. They behave in the same way – first, we get the cheapest or highest price using the LINQ Min()
and Max()
methods. Then, we return a list of tickets with that price using the Where()
method we again get from the System.Linq
namespace.
We can shorten both methods:
public IEnumerable<Ticket> GetCheapestTickets() => Tickets.Where(x => x.Price == Tickets.Min(t => t.Price)); public IEnumerable<Ticket> GetMostExpensiveTickets() => Tickets.Where(x => x.Price == Tickets.Max(t => t.Price));
This reduces our code but makes it harder to understand as we now calculate the cheapest and highest price directly in the Where()
method. This is also a suboptimal solution performance-wise since the min/max value is calculated in each iteration.
Batching
We add a method that gets boarding groups:
public IEnumerable<IEnumerable<Ticket>> GetBordingGroups(int numberOfGroups) { var groups = new List<List<Ticket>>(); var groupSize = (int)Math.Ceiling((double)Tickets.Count / numberOfGroups); for (int i = 0; i < numberOfGroups; i++) { groups.Add(Tickets.Skip(i * groupSize).Take(groupSize).ToList()); } return groups; }
Here, we define the GetBordingGroups()
method that takes one parameter of int
type that determines how many groups we will have. Inside the method, we determine the group size. Then, inside a for
loop, using LINQ’s Skip()
and Take()
methods we create the boarding groups that our method will return.
Ordering and Taking
We add a method that gets passengers for security checks:
public IEnumerable<Ticket> GetPassengersForSecurityCheck() { var passangersForSecurityCheck = new List<Ticket>(); var ticketsInRandomOrder = Tickets.OrderBy(x => Guid.NewGuid()).ToList(); for (int i = 0; i < ticketsInRandomOrder.Count; i += 5) { passangersForSecurityCheck.Add(ticketsInRandomOrder[i]); } return passangersForSecurityCheck; }
We define the GetPassengersForSecurityCheck()
method. It’s responsible for shuffling all tickets and then getting every fifth one. We achieve the shuffling using the OrderBy()
method and randomizing the order base on a new Guid
for each Ticket
. To get every fifth ticket out of our randomized list we use a simple for
loop.
We are done with our classes and methods, let’s see how they behave.
In our Program
class, we create a new Flight
:
var flight = new Flight( 1, "Rome, Italy", "Paris, France", DateTime.UtcNow, DateTime.UtcNow.AddHours(1)); for (int i = 0; i < 30; i++) { flight.AddTicket(GetBogusticket()); }
We declare the flight
variable that holds information about a flight from Rome to Paris. We also add 30 tickets that we generate using the Bogus library.
Next, we test the methods’ behavior:
var cheapestTickets = flight.GetCheapestTickets(); Console.WriteLine($"Cheapest ticket costs: {cheapestTickets.First().Price}."); Console.WriteLine($"{cheapestTickets.Count()} such ticket/s sold."); var mostExpensiveTickets = flight.GetMostExpensiveTickets(); Console.WriteLine($"Most expensive ticket costs: {mostExpensiveTickets.First().Price}."); Console.WriteLine($"{mostExpensiveTickets.Count()} such ticket/s sold."); var boardingGroups = flight.GetBordingGroups(4); Console.WriteLine($"We have {boardingGroups.Count()} boarding groups."); var passengersForSecurityCheck = flight.GetPassengersForSecurityCheck(); Console.WriteLine($"We have {passengersForSecurityCheck.Count()} passengers for security check."); var willTakePlace = flight.WillFligthTakePlace(); Console.WriteLine(willTakePlace); var areThereFreeSeats = flight.AreThereFreeSeats(); Console.WriteLine(areThereFreeSeats);
And examine the result:
Cheapest ticket costs: 21.2155458849548045000. 1 such ticket/s sold. Most expensive ticket costs: 238.73709387928683000. 1 such ticket/s sold. We have 4 boarding groups. We have 6 passengers for security check. True True
First, we see the price of the cheapest and most expensive tickets. Then, we get 4 boarding groups – the count we passed to the method. After that, we see we got 6 passengers for our security check – the exact count we expect when taking every fifth passenger out of the 30 tickets we added. Finally, the WillFligthTakePlace()
and AreThereFreeSeats()
both return true
– we have more than the minimum and less than the maximum number of passengers.
Now, let’s see how we can improve our code.
Install the MoreLINQ Package
The MoreLINQ library is developed and maintained by Atif Aziz with the help of the .NET community.
Let’s install MoreLINQ
via the .NET CLI:
Install-Package MoreLINQ
Once we have done this, we are good to go!
How to Use MoreLINQ to Extend LINQ?
To start using MoreLINQ, we need to add using MoreLinq;
to our file. All of its helpful methods are contained in the MoreEnumerable
class. It and its methods are all static
, so we don’t need to instantiate it before we use it.
To use the methods, first, you need to pass the IEnumerable
or IEnumerable<T>
that you wish to query. Then, depending on the method, you can pass a different number of parameters.
Now that we know the basics, let’s dive into the methods!
Useful MoreLINQ Methods
The MoreLINQ
library provides more methods that we can show, but we will examine a few and use them to improve our Flight
class.
Getting Min/Max Values
The MinBy()
and MaxBy()
methods can be used to directly get the elements that either have the lowest or highest value.
Let’s see how they work:
public IEnumerable<Ticket> GetCheapestTickets() => MoreEnumerable.MinBy(Tickets, x => x.Price); public IEnumerable<Ticket> GetMostExpensiveTickets() => MoreEnumerable.MaxBy(Tickets, x => x.Price);
We update our GetCheapestTickets()
and GetMostExpensiveTickets()
methods. We replaced the previous logic with MinBy()
and MaxBy()
methods to which we first pass the Tickets
list and then a lambda that tells the methods to sort based on price.
Let’s examine the result of calling those methods:
The cheapest ticket costs: 32.932576992329405000, there are 1 such ticket/s. The most expensive ticket costs: 248.10936633726026000, there are 1 such ticket/s.
We see that the functionality remains the same.
Batching
The Batch()
method allows us to easily split a collection, creating different batches. The first parameter is again our Tickets
, the second one is the size of a batch:
public IEnumerable<IEnumerable<Ticket>> GetBordingGroups(int numberOfGroups) { var groupSize = (int)Math.Ceiling((double)Tickets.Count / numberOfGroups); return MoreEnumerable.Batch(Tickets, groupSize); }
Here, we update the GetBordingGroups()
method – the logic that calculates the group sizes remain the same but the rest is reduced to one call.
Next, we examine how it behaves:
We have 4 boarding groups.
Again, we get the same behavior but with less code.
Ordering
The Shuffle()
method provides an easy way to randomize the order of any collection:
public IEnumerable<Ticket> GetPassengersForSecurityCheck() { var passangersForSecurityCheck = new List<Ticket>(); var ticketsInRandomOrder = MoreEnumerable.Shuffle(Tickets); for (int i = 0; i < ticketsInRandomOrder.Count; i += 5) { passangersForSecurityCheck.Add(ticketsInRandomOrder[i]); } return passangersForSecurityCheck; }
In our GetPassangersForSecurityCheck()
method, we replace LINQ’s OrderBy()
with Shuffle()
. The only thing we need to pass here is the list of Tickets.
Taking
The Take
methods in MoreLINQ allow us to take different elements.
TakeLast()
‘s name shows the exact behavior of the method – it returns the last element of a collection, much like the LINQ’s Last()
method. TakeUntil()
takes elements until a given predicate returns true
, for example, we can take tickets until we reach a certain price.
The TakeEvery()
method returns a collection holding every n-th element of a given collection:
public IEnumerable<Ticket> GetPassengersForSecurityCheck() { var ticketsInRandomOrder = MoreEnumerable.Shuffle(Tickets); return MoreEnumerable.TakeEvery(ticketsInRandomOrder, 5); }
Again GetPassengersForSecurityCheck()
method, we remove the logic that takes every fifth ticket and we return the result of the TakeEvery()
method.
Next, we check how the method behaves:
We have 6 passengers for security check.
The result we get doesn’t differ from the original one.
Updating Non-LINQ Methods With MoreLINQ
MoreLINQ can be used to replace not only LINQ methods but other code logic as well:
public bool AreThereFreeSeats() { return MoreEnumerable.AtMost(Tickets, MaxNumberOfTickets - 1); } public bool WillFligthTakePlace() { return MoreEnumerable.CountBetween(Tickets, MinNumberOfTickets, MaxNumberOfTickets); }
First, we update the AreThereFreeSeats()
method. To do so, we use the AtMost()
method and pass it the Tickets
and MaxNumberOfTickets - 1
. We subtract 1 to ensure we have at least one free seat on the plane. Then we use CountBetween()
to update the WillFligthTakePlace()
method. The CountBetween()
method takes in a collection along with a minimum and maximum number of elements.
For the WillFligthTakePlace()
method, we can also use the AtLeast()
method which determines whether a collection has at least the number of elements passed to the method.
All three MoreLINQ methods return a bool
, just what we need for our original methods.
Conclusion
This article taught us that the MoreLINQ library is a powerful tool for developers who use LINQ in their projects. It offers a wide range of additional LINQ operators that can simplify the development process. With MoreLINQ, we can write more concise and expressive code that is easier to read and maintain. It is a must-have for any developer who works with LINQ, and its benefits make it well worth the investment of time and effort to learn and integrate into your workflow.