- Lab
- Core Tech

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.

Path Info
Table of Contents
-
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 thisnamespace 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 commanddotnet 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. -
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 theT
type parameter, which acts as the placeholder in thereplace
method definition but is "filled" in when called. The type parameter doesn't always need to be calledT
, but it's standard practice to useT
or to prefix it before another term, such asTKey
orTValue
.Note that type safety is enforced here because the
item
anditemList
parameters use the sameT
type parameter. This means that something likereplace(2, 5, bookList);
wouldn't work since 5 is not of typeBook
whilebookList
is a list of books. Similarly,replace(2, replacementBook, someListOfIntegers);
wouldn't work because the list of integers isList<int>
whilereplacementBook
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.
-
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 ofT
elements is returned and can later be converted into a collection that implementsIEnumerable<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. Thefrom num in numbers
essentially works as aforeach
statement through each element in the specified collection, followed by any amount of SQL operator statements. In this case, it's a singularwhere num > 5
designed to filter out any number less than 5. The last statement will always be aselect
orgroup
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 theforeach
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 thevar
keyword. The alternative would be to type it asIEnumerable<T>
whereT
isint
in the example, but usingvar
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.
-
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 calledSortTicketsByName
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 calledSortTicketsDepartureDate
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 calledgetTicketByname
or something similar. It should have astring
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 calledReschedule
or something similar. It should also have astring
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. AticketManager
instance has been declared and you will be calling your extension methods through it. In theswitch
statement inMain()
, you will notice that cases 4-8 have a placeholderConsole
statement. Replace these with the correct methods that you implemented.You can look at the
DisplayMenu()
function inTicketManager.cs
to see which option corresponds to each function. For example, case 4 would useticketManager.ViewAllTickets();
. For the last 2 cases, uncomment theConsole
anduserInput
statements. TheuserInput
will be used as the parameter for yourgetTicketByName()
andReschedule()
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.
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.