• Labs icon Lab
  • Core Tech
Labs

Guided: Generics and LINQ in C#

This Code Lab is centered around understanding the concepts of Generics and LINQ in C#. Generics in C# enable the creation of flexible and type-safe code and is closely coupled with LINQ, or Language Integrated Query, a C# feature that provides a unified way to query and manipulate data from various sources using a SQL-like syntax. Throughout this lab you'll understand how LINQ(and to an extent Generics) enhance code efficiency and readability in C# by utilizing it in your code to incorporate additional features within an existing application.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 40m
Published
Clock icon Jan 20, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: Generics and LINQ in C# lab.

    This C# lab will introduce the usage of generics and LINQ (Language-Integrated Query). During this lab, you will delve into the realm of generic programming to understand how to create flexible and reusable code structures. Additionally, you will unravel the capabilities of LINQ, empowering you to seamlessly query and manipulate data. By discovering the synergy between generics and LINQ in this hands-on laboratory experience, you will be enhance your coding efficiency within your C# applications.


    The ticket tracker application you will be working with has most of its behind-the-scenes and main application logic already implemented for you. In this lab, you will be responsible for incorporating LINQ queries to add features such as sorting tickets by name or departure date, viewing the number of tickets in the system through aggregation, and filtering tickets by name for lookup or by ID for rescheduling.

    You can also check the solution directory if you are stuck or want to check your code at any time. Keep in mind that the solution is just one of many so it is completely acceptable for your code to be different as long as it is working as intended.

    Note: The files in the solution directory puts the solution code in the solution namespace for compilation purposes. Do not copy this namespace solution line or it will cause errors when running the application. Only take into account the code within the class block.


    You're invited to experiment and learn interactively. Make use of the Terminal to the right to debug your implementations using dotnet build --no-restore to compile your code or execute the ticket tracker application by entering the command dotnet run --no-restore to verify that your implementations are working as intended. If there are errors, the compiler will notify you where those errors are occurring.

  2. Challenge

    Generics

    Overview

    Generics in C# allow you to define classes and methods(and much more) with a type parameter. A type parameter is used as a placeholder in a C# construct's definition so that a type does not need to be specified during declaration or instantiation. Generics are useful in promoting code reusage and can still be considered type-safe as they must be used appropriately based on their specified type when instantiated or invoked.


    Usage

    Suppose you had a list of books or a list of products in a shopping cart. Normally, if you wanted a way to replace an element at the specified index with another element, you might need something like the following:

    List<Book> bookList; // populate with some books
    List<Product> shoppingCart // populate with some products
    
    public void replaceBook(int index, Book subBook) {
      if(index >= 0 && index < bookList.length) {
        bookList[index] = subBook;
      }
    }
    
    public void replaceProduct(int index, Product subProduct) {
      if(index >= 0 && index < shoppingCart.length) {
        shoppingCart[index] = subProduct;
      }
    }
    
    replaceBook(2, replacementBook);
    replaceProduct(4, replacementProduct);
    

    Evidently this would get progressively redundant if you had more lists of items to manage. Using generics would greatly reduce code redundancy in the following example:

    List<Book> bookList; // populate with some books
    List<Product> shoppingCart // populate with some products
    
    public void replace(int index, T item, List<T> itemList) {
       if(index >= 0 && index < itemList.length) {
        itemList[index] = item;
      }
    }
    
    replace(2, replacementBook, bookList);
    replace(4, replacementProduct, shoppingCart);
    

    As you can see, the each individual replace function has been replaced by a singular replace function that is less code yet can handle books and products, or any other types that you specify. This is done with the T type parameter, which acts as the placeholder in the replace method definition but is "filled" in when called. The type parameter doesn't always need to be called T, but it's standard practice to use T or to prefix it before another term, such as TKey or TValue.

    Note that type safety is enforced here because the item and itemList parameters use the same T type parameter. This means that something like replace(2, 5, bookList); wouldn't work since 5 is not of type Book while bookList is a list of books. Similarly, replace(2, replacementBook, someListOfIntegers); wouldn't work because the list of integers is List<int> while replacementBook is a book.


    Further Details

    While the example above highlighted a use case using a generic method, generics can also be applied to classes, interfaces, properties, etc. Generic classes can serve as base classes or be derived from both generic and non-generic classes. Both generic and non-generic classes can also contain generic methods or properties as well.

    In addition, you can specify multiple type parameters to a generic or even specify certain type constraints to it(eg. T can only be an array, a list, a set, etc).

    // Example generic class
    public class ExampleClass1<T> {
      public T exampleData1 { get; set;}
    }
    
    // Example generic with multiple type parameters
    public class SomeClass<TFirst, TSecond> {
      public TFirst firstData { get; set; }
      public TSecond secondData { get; set; }
    }
    
    // Example generic with multiple parameters and constraint
    public class MultiClass<TFirst, TSecond>
      where TFirst : int, double
      where TSecond : string, bool {
    	
        // Must be an int or double
        public TFirst firstData { get; set; }
    		
        // Must be a string or boolean
        public TSecond secondData { get; set; }
    }
    

    While generics can be used independently, they are also fundamentally compatible with another C# construct that will be covered: LINQ.

  3. Challenge

    LINQ

    Overview

    LINQ(Language Integrated Query) is a powerful built-in feature for the C# language that provides a uniform model for querying and transforming data across multiple formats.

    C# allows you to work with various data sources such as relational databases(SQL), XML, and in-memory collections, but to perform common operations across these data sources may require different syntactic implementations. As seen in the previous section, introducing generics is useful for reducing unnecessary code by creating a solution capable of servicing multiple types. In the context of data sources and collections, LINQ fulfills that role.

    LINQ is built upon generics as LINQ query variables are typed as IEnumerable<T>. This means that when the query is executed, a sequence of T elements is returned and can later be converted into a collection that implements IEnumerable<T>, such as a List. Some of the most common operations LINQ is used for include aggregation(sum, min, max, average, etc.), filtering, and sorting.


    Syntax

    LINQ is written using two formats. The first format is known as query syntax while the second format is known as method syntax.

    // Data source
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    // Query syntax: Filtering numbers greater than 5 
    var query = from num in numbers
                 where num > 5
                 select num;
    
    // Execute Query
    foreach (var squaredNumber in query)
    {
        Console.WriteLine(squaredNumber);
    }
    

    As seen in this query syntax example, the result variable is written in a manner similar to an SQL query. The from num in numbers essentially works as a foreach statement through each element in the specified collection, followed by any amount of SQL operator statements. In this case, it's a singular where num > 5 designed to filter out any number less than 5. The last statement will always be a select or group operator to form the sequence.

    Now for method syntax:

    // Data source
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    // Method syntax: Filtering numbers greater than 5 
    var query = numbers.Where(num => num > 5);
    
    // Execute query
    foreach (var squaredNumber in query)
    {
        Console.WriteLine(squaredNumber);
    }
    

    As you can see in this example, method syntax is no different from calling extension methods as you typically would with any other class. The body of the linq extension method being called usually has a lambda expression to perform whatever action is needed.


    Further Details

    There are a few key details to take note of. First, the query variable in each example is ostensibly the query variable, but the query has not executed yet. Think of it as merely containing the instructions for the query, whereas the results of the query being executed doesn't happen until the foreach statement occurs. This is known is delayed execution. If you want immediate execution, you would need to use method syntax with certain extension methods that either resolve to a single value(Sum, Count, Average, etc.) or have a name prefixed with "To" such as ToList(), ToArray(), etc.

    Second, the query variable uses the var keyword. The alternative would be to type it as IEnumerable<T> where T is int in the example, but using var is oftentimes more readable. The compiler is able to implicitly infer the type based on the LINQ expression.

    Lastly, both method and query syntax are not mutually exclusive. They are interchangeable and the usage of either is purely based on preference or any other pre-established guidelines. Furthermore, they can actually be combined and used together in a single query

    // Data source
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    // Combines query syntax with method syntax
    // Selects the square of each number greater than 5 rather than just the number
    var query = (from num in numbers
                 where num > 5
                 select num * num).Average(); // Method syntax here returns the average of the squares of numbers greater than 5
    
    // No foreach loop is needed because the Average() function performs immediate execution
    
    // query would be of type double instead of IEnumerable<int> because Average() returns a singular double value when used on a sequence of int.
    
  4. Challenge

    The Ticket Application

    Overview

    The ticket tracker application you are working on currently only has 3 features: adding a bus ticket, adding a train ticket, and removing a ticket. When executing the app, you will see options in the menu for 5 more functions, but inputting any of them will tell you that they are not complete. It will be your responsibility in this lab to implement all 5 of them.

    These functions include viewing all tickets in the system, sorting all tickets by customer name or by departure date, searching tickets by customer name, and rescheduling a ticket. To do this, you will be using LINQ queries. You can use either query or method syntax, although if you do use query syntax you will still need to use method syntax for aggregation into a single value.


    TicketManager

    Navigate to TicketManager.cs.

    Go to the function ViewAllTickets at the bottom of the file. This is the first of the five functions you will need to implement.

    Instructions (1) 1. Count the total number of tickets in the system, which is held in `AllTickets`. You can use the LINQ `Count()` method for this. 2. Find all of either the bus OR train tickets in `AllTickets`. Use the LINQ `Where()` extension method or query it. For example, `AllTickets.Where( x => x.GetType().GetProperty("BusNumber") != null)` checks if a ticket contains a `BusNumber` property. If it does, it's a `BusTicket`, otherwise it's a `TrainTicket`. 3. Remember that your query simply returns an `IEnumerable`, that is a sequence of 0 or more tickets that meet the criteria. Use `Count()` method again to find the number of them. 4. Find the number of tickets for the other transportation mode, which can be done using the same tactic you did before or simply subtracting the number of tickets for one transportation from total number of tickets. 5. Print out the total number of tickets along with the number of tickets of each type. For instance, `Console.WriteLine($"Total Number of Tickets: {totalTickets}\nNo. Bus Tickets: {numBusTickets}\nNo. Train Tickets: {numTrainTickets}\n");` 6. Use the `ForEach()` method on `AllTickets` to display each ticket's information using `ShowInfo()` on each ticket. `ForEach()` utilizes a lambda expression just like `Where()` does.

    Next, create a new public void method called SortTicketsByName or something similar. This is the second of the five functions you will need to implement.

    Instructions (2) 1. Use the `OrderBy()` LINQ method on `AllTickets`. `OrderBy` also takes a lambda expression, in this case you would use the `CustomerName` on each ticket. This will sort the tickets by alphabetical name. 2. Convert this `IEnumerable` to a `List` using `ToList()`. Then set `AllTickets` to this list. 3. For formatting and visibility, you can add a console statement such as `Console.WriteLine("\nTickets have now been sorted by alphabetically.\n----------");` after.

    Next, create a new public void method called SortTicketsDepartureDate or something similar. This is the third of the five functions you will need to implement.

    Instructions (3) 1. Use the `OrderBy()` LINQ method on `AllTickets` again. This is just like `SortTicketsByName`, but this time you will compare `DepartDate` instead of `CustomerName`. This will sort the tickets by departure date. 2. Convert this `IEnumerable` to a `List` using `ToList()`. Then set `AllTickets` to this list. 3. For formatting and visibility, you can add a console statement such as `Console.WriteLine("\nTickets have now been sorted by departure date.\n----------");` after.

    Next, create a new public void method called getTicketByname or something similar. It should have a string parameter. This is the fourth of the five functions you will need to implement.

    Instructions (4) 1. Query `AllTickets` using `Where()` to check whether the `CustomerName` of the ticket contains the string parameter. For instance, `$"{x.CustomerName!.ToLower()}".Contains(name.ToLower())` provides a case-insensitive check. In this example, `name` is the string parameter. 2. Convert your query results to a list. 3. If there are no query results, ie. no tickets have the given name, print something along the lines of `Console.WriteLine("\nNo tickets found with the given name.");`. You can use the `Count` property of a List, which is not the same as the `Count()` method. 4. Otherwise, print out each ticket's information using `ForEach()` and `ShowInfo()`.

    Next, create a new public void method called Reschedule or something similar. It should also have a string parameter. This is the last of the five functions you will need to implement.

    Instructions (5) 1. Query `AllTickets` using `FirstOrDefault()`. This returns the first matching result, or `null` if no match is found. Since ticket ids are supposed to be unique, there should only be a singular result 2. If there is no match, print something similar to the previous step. 3. Otherwise, retrieve the `CustomerName` from the ticket and check whether it is a `BusTicket` or `TrainTicket`. Then remove the ticket from `AllTickets` using `Remove()`. 4. Add a new ticket by using the `AddNewTicket(int ticketType, string customerName, false)` method that is defined earlier in the class. The `ticketType` should be 1 of it's a `BusTicket` or 2 if it's a `TrainTicket`. You should do `false` for the third parameter so the method does not print out the default message as that is usually meant for adding a new ticket through options 1 or 2 in the menu. 5. Instead, print out a message after along the lines of `Console.WriteLine($"\nTicket with id {id} for {customer} has been rescheduled.");`

    With that, TicketManager.cs should be complete.


    Program

    Now in Program.cs, you will add your function implementations to the main application logic. A ticketManager instance has been declared and you will be calling your extension methods through it. In the switch statement in Main(), you will notice that cases 4-8 have a placeholder Console statement. Replace these with the correct methods that you implemented.

    You can look at the DisplayMenu() function in TicketManager.cs to see which option corresponds to each function. For example, case 4 would use ticketManager.ViewAllTickets();. For the last 2 cases, uncomment the Console and userInput statements. The userInput will be used as the parameter for your getTicketByName() and Reschedule() methods.


    All set! Now all 5 functions that you implemented should be working properly when running the application.

    To reiterate, Generics are a key concept for programming that allow you to define data and algorithms across multiple data types. This provides the foundation for LINQ queries to function, allowing you to write code that is more intuitive and readable.

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.