In this article, we’ll explore the capabilities of the MoreLINQ library, which enables us to improve the already robust LINQ functionality. 

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!