Category Archives: Uncategorized

Pro ASP.NET Core 7 Chapter 7 SportsStore: A real application

7 SportsStore: A real application

This chapter covers
• Creating the SportsStore ASP.NET Core project
• Adding a data model and support for a database
• Displaying a basic product catalog
• Paginating data
• Styling content

In the previous chapters, I built quick and simple ASP.NET Core applications. I described ASP.NET Core patterns, the essential C# features, and the tools that good ASP.NET Core developers require. Now it is time to put everything together and build a simple but realistic e-commerce application.

My application, called SportsStore, will follow the classic approach taken by online stores everywhere. I will create an online product catalog that customers can browse by category and page, a shopping cart where users can add and remove products, and a checkout where customers can enter their shipping details. I will also create an administration area that includes create, read, update, and delete (CRUD) facilities for managing the catalog, and I will protect it so that only logged-in administrators can make changes.

My goal in this chapter and those that follow is to give you a sense of what real ASP.NET Core development is by creating as realistic an example as possible. I want to focus on ASP.NET Core, of course, so I have simplified the integration with external systems, such as the database, and omitted others entirely, such as payment processing.

You might find the going a little slow as I build up the levels of infrastructure I need, but the initial investment will result in maintainable, extensible, well-structured code with excellent support for unit testing.

Unit testing
I include sections on unit testing different components in the SportsStore application throughout the development process, demonstrating how to isolate and test different ASP.NET Core components.
I know that unit testing is not embraced by everyone. If you do not want to unit test, that is fine with me. To that end, when I have something to say that is purely about testing, I put it in a sidebar like this one. If you are not interested in unit testing, you can skip right over these sections, and the SportsStore application will work just fine. You do not need to do any kind of unit testing to get the technology benefits of ASP.NET Core, although, of course, support for testing is a key reason for adopting ASP.NET Core in many projects.

Most of the features I use for the SportsStore application have their own chapters later in the book. Rather than duplicate everything here, I tell you just enough to make sense of the example application and point you to another chapter for in-depth information.

I will call out each step needed to build the application so that you can see how the ASP.NET Core features fit together. You should pay particular attention when I create views. You will get some odd results if you do not follow the examples closely.

7.1 Creating the projects

I am going to start with a minimal ASP.NET Core project and add the features I require as they are needed. Open a new PowerShell command prompt from the Windows Start menu and run the commands shown in listing 7.1 to get started.

TIP You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/manningbooks/pro-asp.net-core-7. See chapter 1 for how to get help if you have problems running the examples.

Listing 7.1 Creating the SportsStore project

dotnet new globaljson --sdk-version 7.0.100 --output SportsSln/SportsStore
dotnet new web --no-https --output SportsSln/SportsStore --framework net7.0
dotnet new sln -o SportsSln 

dotnet sln SportsSln add SportsSln/SportsStore

These commands create a SportsSln solution folder that contains a SportsStore project folder created with the web project template. The SportsSln folder also contains a solution file, to which the SportsStore project is added.

I am using different names for the solution and project folders to make the examples easier to follow, but if you create a project with Visual Studio, the default is to use the same name for both folders. There is no “right” approach, and you can use whatever names suit your project.

7.1.1 Creating the unit test project

To create the unit test project, run the commands shown in listing 7.2 in the same location you used for the commands shown in listing 7.1.

Listing 7.2 Creating the unit test project

dotnet new xunit -o SportsSln/SportsStore.Tests --framework net7.0
dotnet sln SportsSln add SportsSln/SportsStore.Tests
dotnet add SportsSln/SportsStore.Tests reference SportsSln/SportsStore

I am going to use the Moq package to create mock objects. Run the command shown in listing 7.3 to install the Moq package into the unit testing project. Run this command from the same location as the commands in listing 7.2.

Listing 7.3 Installing the Moq package

dotnet add SportsSln/SportsStore.Tests package Moq --version 4.18.4

7.1.2 Opening the projects

If you are using Visual Studio Code, select File > Open Folder, navigate to the SportsSln folder, and click the Select Folder button. Visual Studio Code will open the folder and discover the solution and project files. When prompted, as shown in figure 7.1, click Yes to install the assets required to build the projects. Select SportsStore if Visual Studio Code prompts you to select the project to run.

If you are using Visual Studio, click the “Open a project or solution” button on the splash screen or select File > Open > Project/Solution. Select the SportsSln.sln file in the SportsSln folder and click the Open button to open the project.

Figure 7.1 Adding assets in Visual Studio Code

7.1.3 Configuring the HTTP port

To configure the HTTP port that ASP.NET Core will use to listen for HTTP requests, make the changes shown in listing 7.4 to the launchSettings.json file in the SportsStore/Properties folder.

Listing 7.4 Setting the HTTP Port in the launchSettings.json File in the SportsStore/ Properties Folder

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 0
    }
  },
  "profiles": {
    "SportsStore": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

7.1.4 Creating the application project folders

The next step is to create folders that will contain the application’s components. Right-click the SportsStore item in the Visual Studio Solution Explorer or Visual Studio Code Explorer pane and select Add > New Folder or New Folder to create the set of folders described in table 7.1.

Table 7.1 The application project folders

Name Description
Models This folder will contain the data model and the classes that provide access to the data in the application’s database.
Controllers This folder will contain the controller classes that handle HTTP requests.
Views This folder will contain all the Razor files, grouped into separate subfolders.
Views/Home This folder will contain Razor files that are specific to the Home controller, which I create in the “Creating the Controller and View” section.
Views/Shared This folder will contain Razor files that are common to all controllers.

7.1.5 Preparing the services and the request pipeline

The Program.cs file is used to configure the ASP.NET Core application. Apply the changes shown in listing 7.5 to the Program.cs file in the SportsStore project to configure the basic application features.

NOTE The Program.cs file is an important ASP.NET Core feature. I describe it in detail in part 2.

Listing 7.5 Configuring the application in the Program.cs file in the SportsStore folder

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();
//app.MapGet("/", () => "Hello World!");

app.UseStaticFiles();
app.MapDefaultControllerRoute();

app.Run();

The builder.Services property is used to set up objects, known as services, that can be used throughout the application and that are accessed through a feature called dependency injection, which I describe in chapter 14. The AddControllersWithViews method sets up the shared objects required by applications using the MVC Framework and the Razor view engine.

ASP.NET Core receives HTTP requests and passes them along a request pipeline, which is populated with middleware components registered using the app property. Each middleware component is able to inspect requests, modify them, generate a response, or modify the responses that other components have produced. The request pipeline is the heart of ASP.NET Core, and I describe it in detail in chapter 12, where I also explain how to create custom middleware components.

The UseStaticFiles method enables support for serving static content from the wwwroot folder and will be created later in the chapter.

One especially important middleware component provides the endpoint routing feature, which matches HTTP requests to the application features—known as endpoints—able to produce responses for them, a process I describe in detail in chapter 13. The endpoint routing feature is added to the request pipeline automatically, and the MapDefaultControllerRoute registers the MVC Framework as a source of endpoints using a default convention for mapping requests to classes and methods.

7.1.6 Configuring the Razor view engine

The Razor view engine is responsible for processing view files, which have the .cshtml extension, to generate HTML responses. Some initial preparation is required to configure Razor to make it easier to create views for the application.

Add a Razor View Imports file named _ViewImports.cshtml in the Views folder with the content shown in listing 7.6.

CAUTION Pay close attention to the contents of this file. It is easy to make a mistake that causes the application to generate incorrect HTML content.

Listing 7.6 The contents of the _ViewImports.cshtml file in the SportsStore/ Views folder

@using SportsStore.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The @using statement will allow me to use the types in the SportsStore.Models namespace in views without needing to refer to the namespace. The @addTagHelper statement enables the built-in tag helpers, which I use later to create HTML elements that reflect the configuration of the SportsStore application and which I describe in detail in chapter 15. (You may see a warning or error displayed by the code editor for the contents of this file, but this will be resolved shortly and can be ignored.)

Add a Razor View Start file named _ViewStart.cshtml to the SportsStore/Views folder with the content shown in listing 7.7. (The file will already contain this expression if you create the file using the Visual Studio item template.)

Listing 7.7 The contents of the _ViewStart.cshtml file in the SportsStore/Views folder

@{
    Layout = "_Layout";
}

The Razor View Start file tells Razor to use a layout file in the HTML that it generates, reducing the amount of duplication in views. To create the view, add a Razor layout named _Layout.cshtml to the Views/Shared folder, with the content shown in listing 7.8.

Listing 7.8 The contents of the _Layout.cshtml file in the SportsStore/Views/Shared folder

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>SportsStore</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

This file defines a simple HTML document into which the contents of other views will be inserted by the @RenderBody expression. I explain how Razor expressions work in detail in chapter 21.

7.1.7 Creating the controller and view

Add a class file named HomeController.cs in the SportsStore/Controllers folder and use it to define the class shown in listing 7.9. This is a minimal controller that contains just enough functionality to produce a response.

Listing 7.9 The contents of the HomeController.cs file in the SportsStore/Controllers folder

using Microsoft.AspNetCore.Mvc;

namespace SportsStore.Controllers {
    public class HomeController: Controller {

        public IActionResult Index() => View();

    }
}

The MapDefaultControllerRoute method used in listing 7.5 tells ASP.NET Core how to match URLs to controller classes. The configuration applied by that method declares that the Index action method defined by the Home controller will be used to handle requests.

The Index action method doesn’t do anything useful yet and just returns the result of calling the View method, which is inherited from the Controller base class. This result tells ASP.NET Core to render the default view associated with the action method. To create the view, add a Razor View file named Index.cshtml to the Views/Home folder with the content shown in listing 7.10.

Listing 7.10 The contents of the Index.cshtml file in the SportsStore/Views/Home folder

<h4>Welcome to SportsStore</h4>

7.1.8 Starting the data model

Almost all projects have a data model of some sort. Since this is an e-commerce application, the most obvious model I need is for a product. Add a class file named Product.cs to the Models folder and use it to define the class shown in listing 7.11.

Listing 7.11 The contents of the Product.cs file in the SportsStore/Models folder

using System.ComponentModel.DataAnnotations.Schema;

namespace SportsStore.Models {

    public class Product {

        public long? ProductID { get; set; }

        public string Name { get; set; } = String.Empty;

        public string Description { get; set; } = String.Empty;

        [Column(TypeName = "decimal(8, 2)")]
        public decimal Price { get; set; }

        public string Category { get; set; } = String.Empty;
    }
}

The Price property has been decorated with the Column attribute to specify the SQL data type that will be used to store values for this property. Not all C# types map neatly onto SQL types, and this attribute ensures the database uses an appropriate type for the application data.
7.1.9 Checking and running the application

Before going any further, it is a good idea to make sure the application builds and runs as expected. Run the command shown in listing 7.12 in the SportsStore folder.

Listing 7.12 Running the example application

dotnet run

Request http://localhost:5000, and you will see the response shown in figure 7.2.

Figure 7.2 Running the example application

7.2 Adding data to the application

Now that the SportsStore contains some basic setup and can produce a simple response, it is time to add some data so that the application has something more useful to display. The SportsStore application will store its data in a SQL Server LocalDB database, which is accessed using Entity Framework Core. Entity Framework Core is the Microsoft object-to-relational mapping (ORM) framework, and it is the most widely used method of accessing databases in ASP.NET Core projects.

Caution If you did not install LocalDB when you prepared your development environment in chapter 2, you must do so now. The SportsStore application will not work without its database.
7.2.1 Installing the Entity Framework Core packages

The first step is to add Entity Framework Core to the project. Use a PowerShell command prompt to run the command shown in listing 7.13 in the SportsStore folder. If you receive an error asking you to specify a project, then delete the SportsStore - Backup.csproj file in the SportsStore folder and try again.

Listing 7.13 Adding the Entity Framework Core packages to the SportsStore project

dotnet add package Microsoft.EntityFrameworkCore.Design --version 7.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0

These packages install Entity Framework Core and the support for using SQL Server. Entity Framework Core also requires a tools package, which includes the command-line tools required to prepare and create databases for ASP.NET Core applications. Run the commands shown in listing 7.14 to remove any existing version of the tools package, if there is one, and install the version used in this book. (Since this package is installed globally, you can run these commands in any folder.)

Listing 7.14 Installing the Entity Framework Core tool package

dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 7.0.0

7.2.2 Defining the connection string

Configuration settings, such as database connection strings, are stored in JSON configuration files. To describe the connection to the database that will be used for the SportsStore data, add the entries shown in listing 7.15 to the appsettings.json file in the SportsStore folder.

The project also contains an appsettings.Development.json file that contains configuration settings that are used only in development. This file is displayed as nested within the appsettings.json file by Solution Explorer but is always visible in Visual Studio Code. I use only the appsettings.json file for the development of the SportsStore project, but I explain the relationship between the files and how they are both used in detail in chapter 15.

TIP Connection strings must be expressed as a single unbroken line, which is fine in the code editor but doesn’t fit on the printed page and is the cause of the awkward formatting in listing 7.15. When you define the connection string in your own project, make sure that the value of the SportsStoreConnection item is on a single line.

Listing 7.15 Adding a configuration setting in the appsettings.json file in the SportsStore folder

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SportsStoreConnection": "Server=(localdb)\\MSSQLLocalDB;Database=SportsStore;MultipleActiveResultSets=true"
  }
}

This configuration string specifies a LocalDB database called SportsStore and enables the multiple active result set (MARS) feature, which is required for some of the database queries that will be made by the SportsStore application using Entity Framework Core.

Pay close attention when you add the configuration setting. JSON data must be expressed exactly as shown in the listing, which means you must ensure you correctly quote the property names and values. You can download the configuration file from the GitHub repository if you have difficulty.

TIP Each database server requires its own connection string format. A helpful site for formulating connection strings is https://www.connectionstrings.com.

7.2.3 Creating the database context class

Entity Framework Core provides access to the database through a context class. Add a class file named StoreDbContext.cs to the Models folder and use it to define the class shown in listing 7.16.

Listing 7.16 The contents of the StoreDbContext.cs file in the SportsStore/Models folder

using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models {
    public class StoreDbContext : DbContext {

        public StoreDbContext(DbContextOptions<StoreDbContext> options)
            : base(options) { }

        public DbSet<Product> Products => Set<Product>();
    }
}

The DbContext base class provides access to the Entity Framework Core’s underlying functionality, and the Products property will provide access to the Product objects in the database. The StoreDbContext class is derived from DbContext and adds the properties that will be used to read and write the application’s data. There is only one property for now, which will provide access to Product objects.
7.2.4 Configuring Entity Framework Core

Entity Framework Core must be configured so that it knows the type of database to which it will connect, which connection string describes that connection, and which context class will present the data in the database. Listing 7.17 shows the required changes to the Program.cs file.

Listing 7.17 Configuring Entity Framework Core in the Program.cs file in the SportsStore folder

using Microsoft.EntityFrameworkCore;
using SportsStore.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<StoreDbContext>(opts => {
    opts.UseSqlServer(
        builder.Configuration["ConnectionStrings:SportsStoreConnection"]);
});

var app = builder.Build();

app.UseStaticFiles();
app.MapDefaultControllerRoute();

app.Run();

The IConfiguration interface provides access to the ASP.NET Core configuration system, which includes the contents of the appsettings.json file and which I describe in detail in chapter 15. Access to the configuration data is through the builder.Configuration property, which allows the database connection string to be obtained. Entity Framework Core is configured with the AddDbContext method, which registers the database context class and configures the relationship with the database. The UseSQLServer method declares that SQL Server is being used.

7.2.5 Creating a repository

The next step is to create a repository interface and implementation class. The repository pattern is one of the most widely used, and it provides a consistent way to access the features presented by the database context class. Not everyone finds a repository useful, but my experience is that it can reduce duplication and ensures that operations on the database are performed consistently. Add a class file named IStoreRepository.cs to the Models folder and use it to define the interface shown in listing 7.18.

Listing 7.18 The contents of the IStoreRepository.cs file in the SportsStore/Models folder

namespace SportsStore.Models {
    public interface IStoreRepository {
        IQueryable<Product> Products { get; }
    }
}

This interface uses IQueryable<T> to allow a caller to obtain a sequence of Product objects. The IQueryable<T> interface is derived from the more familiar IEnumerable<T> interface and represents a collection of objects that can be queried, such as those managed by a database.

A class that depends on the IStoreRepository interface can obtain Product objects without needing to know the details of how they are stored or how the implementation class will deliver them.

Understanding IEnumerable<T> and IQueryable<T> interfaces

The IQueryable<T> interface is useful because it allows a collection of objects to be queried efficiently. Later in this chapter, I add support for retrieving a subset of Product objects from a database, and using the IQueryable<T> interface allows me to ask the database for just the objects that I require using standard LINQ statements and without needing to know what database server stores the data or how it processes the query. Without the IQueryable<T> interface, I would have to retrieve all of the Product objects from the database and then discard the ones that I don’t want, which becomes an expensive operation as the amount of data used by an application increases. It is for this reason that the IQueryable<T> interface is typically used instead of IEnumerable<T> in database repository interfaces and classes.

However, care must be taken with the IQueryable<T> interface because each time the collection of objects is enumerated, the query will be evaluated again, which means that a new query will be sent to the database. This can undermine the efficiency gains of using IQueryable<T>. In such situations, you can convert the IQueryable<T> interface to a more predictable form using the ToList or ToArray extension method.

To create an implementation of the repository interface, add a class file named EFStoreRepository.cs in the Models folder and use it to define the class shown in listing 7.19.

Listing 7.19 The contents of the EFStoreRepository.cs file in the SportsStore/Models folder

namespace SportsStore.Models {
    public class EFStoreRepository : IStoreRepository {
        private StoreDbContext context;

        public EFStoreRepository(StoreDbContext ctx) {
            context = ctx;
        }

        public IQueryable<Product> Products => context.Products;
    }
}

I’ll add additional functionality as I add features to the application, but for the moment, the repository implementation just maps the Products property defined by the IStoreRepository interface onto the Products property defined by the StoreDbContext class. The Products property in the context class returns a DbSet<Product> object, which implements the IQueryable<T> interface and makes it easy to implement the repository interface when using Entity Framework Core.

Earlier in the chapter, I explained that ASP.NET Core supports services that allow objects to be accessed throughout the application. One benefit of services is they allow classes to use interfaces without needing to know which implementation class is being used. I explain this in detail in chapter 14, but for the SportsStore chapters, it means that application components can access objects that implement the IStoreRepository interface without knowing that it is the EFStoreRepository implementation class they are using. This makes it easy to change the implementation class the application uses without needing to make changes to the individual components. Add the statement shown in listing 7.20 to the Program.cs file to create a service for the IStoreRepository interface that uses EFStoreRepository as the implementation class.

TIP Don’t worry if this doesn’t make sense right now. This topic is one of the most confusing aspects of working with ASP.NET Core, and it can take a while to understand.

Listing 7.20 Creating the repository service in the Program.cs file in the SportsStore folder

using Microsoft.EntityFrameworkCore;
using SportsStore.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<StoreDbContext>(opts => {
    opts.UseSqlServer(
        builder.Configuration["ConnectionStrings:SportsStoreConnection"]);
});

builder.Services.AddScoped<IStoreRepository, EFStoreRepository>();

var app = builder.Build();

app.UseStaticFiles();
app.MapDefaultControllerRoute();

app.Run();

The AddScoped method creates a service where each HTTP request gets its own repository object, which is the way that Entity Framework Core is typically used.

7.2.6 Creating the database migration

Entity Framework Core can generate the schema for the database using the data model classes through a feature called migrations. When you prepare a migration, Entity Framework Core creates a C# class that contains the SQL commands required to prepare the database. If you need to modify your model classes, then you can create a new migration that contains the SQL commands required to reflect the changes. In this way, you don’t have to worry about manually writing and testing SQL commands and can just focus on the C# model classes in the application.

Entity Framework Core commands are performed from the command line. Open a PowerShell command prompt and run the command shown in listing 7.21 in the SportsStore folder to create the migration class that will prepare the database for its first use.

Listing 7.21 Creating the database migration

dotnet ef migrations add Initial

When this command has finished, the SportsStore project will contain a Migrations folder. This is where Entity Framework Core stores its migration classes. One of the file names will be a timestamp followed by _Initial.cs, and this is the class that will be used to create the initial schema for the database. If you examine the contents of this file, you can see how the Product model class has been used to create the schema.

7.2.7 Creating seed data

To populate the database and provide some sample data, I added a class file called SeedData.cs to the Models folder and defined the class shown in listing 7.22.

Listing 7.22 The contents of the SeedData.cs file in the SportsStore/Models folder

using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models {

    public static class SeedData {

        public static void EnsurePopulated(IApplicationBuilder app) {
            StoreDbContext context = app.ApplicationServices
                .CreateScope().ServiceProvider
                .GetRequiredService<StoreDbContext>();

            if (context.Database.GetPendingMigrations().Any()) {
                context.Database.Migrate();
            }

            if (!context.Products.Any()) {
                context.Products.AddRange(
                    new Product {
                        Name = "Kayak", Description = 
                            "A boat for one person",
                        Category = "Watersports", Price = 275
                    },
                    new Product {
                        Name = "Lifejacket",
                        Description = "Protective and fashionable",
                        Category = "Watersports", Price = 48.95m
                    },
                    new Product {
                        Name = "Soccer Ball",
                        Description = "FIFA-approved size and weight",
                        Category = "Soccer", Price = 19.50m
                    },
                    new Product {
                        Name = "Corner Flags",
                        Description = 
                          "Give your playing field a professional touch",
                        Category = "Soccer", Price = 34.95m
                    },
                    new Product {
                        Name = "Stadium",
                        Description = "Flat-packed 35,000-seat stadium",
                        Category = "Soccer", Price = 79500
                    },
                    new Product {
                        Name = "Thinking Cap",
                        Description = "Improve brain efficiency by 75%",
                        Category = "Chess", Price = 16
                    },
                    new Product {
                        Name = "Unsteady Chair",
                        Description = 
                          "Secretly give your opponent a disadvantage",
                        Category = "Chess", Price = 29.95m
                    },
                    new Product {
                        Name = "Human Chess Board",
                        Description = "A fun game for the family",
                        Category = "Chess", Price = 75
                    },
                    new Product {
                        Name = "Bling-Bling King",
                        Description = "Gold-plated, diamond-studded King",
                        Category = "Chess", Price = 1200
                    }
                );
                context.SaveChanges();
            }
        }
    }
}

The static EnsurePopulated method receives an IApplicationBuilder argument, which is the interface used in the Program.cs file to register middleware components to handle HTTP requests. IApplicationBuilder also provides access to the application’s services, including the Entity Framework Core database context service.

The EnsurePopulated method obtains a StoreDbContext object through the IApplicationBuilder interface and calls the Database.Migrate method if there are any pending migrations, which means that the database will be created and prepared so that it can store Product objects. Next, the number of Product objects in the database is checked. If there are no objects in the database, then the database is populated using a collection of Product objects using the AddRange method and then written to the database using the SaveChanges method.

The final change is to seed the database when the application starts, which I have done by adding a call to the EnsurePopulated method from the Program.cs file, as shown in listing 7.23.

Listing 7.23 Seeding the database in the Program.cs file in the SportsStore folder

using Microsoft.EntityFrameworkCore;
using SportsStore.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<StoreDbContext>(opts => {
    opts.UseSqlServer(
        builder.Configuration["ConnectionStrings:SportsStoreConnection"]);
});

builder.Services.AddScoped<IStoreRepository, EFStoreRepository>();

var app = builder.Build();

app.UseStaticFiles();
app.MapDefaultControllerRoute();

SeedData.EnsurePopulated(app);

app.Run();

Resetting the database

If you need to reset the database, then run this command in the SportsStore folder:

...
dotnet ef database drop --force --context StoreDbContext
...

Start ASP.NET Core, and the database will be re-created and seeded with data.

7.3 Displaying a list of products

As you have seen, the initial preparation work for an ASP.NET Core project can take some time. But the good news is that once the foundation is in place, the pace improves, and features are added more rapidly. In this section, I am going to create a controller and an action method that can display details of the products in the repository.

Using the Visual Studio scaffolding

As I noted in chapter 4, Visual Studio supports scaffolding to add items to a project.

I don’t use scaffolding in this book. The code and markup that the scaffolding generates are so generic as to be all but useless, and the scenarios that are supported are narrow and don’t address common development problems. My goal in this book is not only to make sure you know how to create ASP.NET Core applications but also to explain how everything works behind the scenes, and that is harder to do when responsibility for creating components is handed to the scaffolding.

If you are using Visual Studio, add items to the project by right-clicking a folder in the Solution Explorer, selecting Add > New Item from the pop-up menu, and then choosing an item template from the Add New Item window.

You may find your development style to be different from mine, and you may find that you prefer working with the scaffolding in your own projects. That’s perfectly reasonable, although I recommend you take the time to understand what the scaffolding does so you know where to look if you don’t get the results you expect.

7.3.1 Preparing the controller

Add the statements shown in listing 7.24 to prepare the controller to display the list of products.

Listing 7.24 Preparing the controller in the HomeController.cs file in the SportsStore/Controllers folder

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers {
    public class HomeController : Controller {
        private IStoreRepository repository;

        public HomeController(IStoreRepository repo) {
            repository = repo;
        }

        public IActionResult Index() => View(repository.Products);
    }
}

When ASP.NET Core needs to create a new instance of the HomeController class to handle an HTTP request, it will inspect the constructor and see that it requires an object that implements the IStoreRepository interface. To determine what implementation class should be used, ASP.NET Core consults the configuration created in the Program.cs file, which tells it that EFStoreRepository should be used and that a new instance should be created for every request. ASP.NET Core creates a new EFStoreRepository object and uses it to invoke the HomeController constructor to create the controller object that will process the HTTP request.

This is known as dependency injection, and its approach allows the HomeController object to access the application’s repository through the IStoreRepository interface without knowing which implementation class has been configured. I could reconfigure the service to use a different implementation class—one that doesn’t use Entity Framework Core, for example—and dependency injection means that the controller will continue to work without changes.

NOTE Some developers don’t like dependency injection and believe it makes applications more complicated. That’s not my view, but if you are new to dependency injection, then I recommend you wait until you have read chapter 14 before you make up your mind.

Unit test: repository access

I can unit test that the controller is accessing the repository correctly by creating a mock repository, injecting it into the constructor of the HomeController class, and then calling the Index method to get the response that contains the list of products. I then compare the Product objects I get to what I would expect from the test data in the mock implementation. See chapter 6 for details of how to set up unit tests. Here is the unit test I created for this purpose, in a class file called HomeControllerTests.cs that I added to the SportsStore.Tests project:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SportsStore.Controllers;
using SportsStore.Models;
using Xunit;

namespace SportsStore.Tests {

    public class HomeControllerTests {

        [Fact]
        public void Can_Use_Repository() {
            // Arrange
            Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
            mock.Setup(m => m.Products).Returns((new Product[] {
                new Product {ProductID = 1, Name = "P1"},
                new Product {ProductID = 2, Name = "P2"}
            }).AsQueryable<Product>());

            HomeController controller = new HomeController(mock.Object);

            // Act
            IEnumerable<Product>? result =
                (controller.Index() as ViewResult)?.ViewData.Model
                     as IEnumerable<Product>;

            // Assert
            Product[] prodArray = result?.ToArray() 
                ?? Array.Empty<Product>();
            Assert.True(prodArray.Length == 2);
            Assert.Equal("P1", prodArray[0].Name);
            Assert.Equal("P2", prodArray[1].Name);
        }
    }
}

It is a little awkward to get the data returned from the action method. The result is a ViewResult object, and I have to cast the value of its ViewData.Model property to the expected data type. I explain the different result types that can be returned by action methods and how to work with them in part 2.

7.3.2 Updating the view

The Index action method in listing 7.24 passes the collection of Product objects from the repository to the View method, which means these objects will be the view model that Razor uses when it generates HTML content from the view. Make the changes to the view shown in listing 7.25 to generate content using the Product view model objects.

Listing 7.25 Using the product data in the Index.cshtml file in the SportsStore/Views/Home folder

@model IQueryable<Product>

@foreach (var p in Model ?? Enumerable.Empty<Product>()) {
    <div>
        <h3>@p.Name</h3>
        @p.Description
        <h4>@p.Price.ToString("c")</h4>
    </div>
}

The @model expression at the top of the file specifies that the view expects to receive a sequence of Product objects from the action method as its model data. I use an @foreach expression to work through the sequence and generate a simple set of HTML elements for each Product object that is received.

There is a quirk in the way that Razor Views work that means the model data is always nullable, even when the type specified by the @model expression is not. For this reason, I use the null-coalescing operator in the @foreach expression with an empty enumeration.

The view doesn’t know where the Product objects came from, how they were obtained, or whether they represent all the products known to the application. Instead, the view deals only with how details of each Product are displayed using HTML elements.

TIP I converted the Price property to a string using the ToString("c") method, which renders numerical values as currency according to the culture settings that are in effect on your server. For example, if the server is set up as en-US, then (1002.3).ToString("c") will return $1,002.30, but if the server is set to en-GB, then the same method will return £1,002.30.

7.3.3 Running the application

Start ASP.NET Core and request http://localhost:5000 to see the list of products, which is shown in figure 7.3. This is the typical pattern of development for ASP.NET Core. An initial investment of time setting everything up is necessary, and then the basic features of the application snap together quickly.

Figure 7.3 Displaying a list of products

7.4 Adding pagination

You can see from figure 7.3 that the Index.cshtml view displays the products in the database on a single page. In this section, I will add support for pagination so that the view displays a smaller number of products on a page and so the user can move from page to page to view the overall catalog. To do this, I am going to add a parameter to the Index method in the Home controller, as shown in listing 7.26.

Listing 7.26 Adding pagination in the HomeController.cs file in the SportsStore/Controllers folder

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers {
    public class HomeController : Controller {
        private IStoreRepository repository;

        public int PageSize = 4;

        public HomeController(IStoreRepository repo) {
            repository = repo;
        }

        public ViewResult Index(int productPage = 1)
            => View(repository.Products
                .OrderBy(p => p.ProductID)
                .Skip((productPage - 1) * PageSize)
                .Take(PageSize));
    }
}

The PageSize field specifies that I want four products per page. I have added an optional parameter to the Index method, which means that if the method is called without a parameter, the call is treated as though I had supplied the value specified in the parameter definition, with the effect that the action method displays the first page of products when it is invoked without an argument. Within the body of the action method, I get the Product objects, order them by the primary key, skip over the products that occur before the start of the current page, and take the number of products specified by the PageSize field.

Unit test: pagination

I can unit test the pagination feature by mocking the repository, requesting a specific page from the controller, and making sure I get the expected subset of the data. Here is the unit test I created for this purpose and added to the HomeControllerTests.cs file in the SportsStore.Tests project:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SportsStore.Controllers;
using SportsStore.Models;
using Xunit;

namespace SportsStore.Tests {

    public class HomeControllerTests {

        [Fact]
        public void Can_Use_Repository() {
            // ...statements omitted for brevity...
        }

        [Fact]
        public void Can_Paginate() {
            // Arrange
            Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
            mock.Setup(m => m.Products).Returns((new Product[] {
                new Product {ProductID = 1, Name = "P1"},
                new Product {ProductID = 2, Name = "P2"},
                new Product {ProductID = 3, Name = "P3"},
                new Product {ProductID = 4, Name = "P4"},
                new Product {ProductID = 5, Name = "P5"}
            }).AsQueryable<Product>());

            HomeController controller = new HomeController(mock.Object);
            controller.PageSize = 3;

            // Act
            IEnumerable<Product> result =
                (controller.Index(2) as ViewResult)?.ViewData.Model
                    as IEnumerable<Product> 
                         ?? Enumerable.Empty<Product>();

            // Assert
            Product[] prodArray = result.ToArray();
            Assert.True(prodArray.Length == 2);
            Assert.Equal("P4", prodArray[0].Name);
            Assert.Equal("P5", prodArray[1].Name);
        }
    }
}

You can see the new test follows the pattern of the existing one, relying on Moq to provide a known set of data with which to work.

7.4.1 Displaying page links

Restart ASP.NET Core and request http://localhost:5000, and you will see that there are now four items shown on the page, as shown in figure 7.4. If you want to view another page, you can append query string parameters to the end of the URL, like this:http://localhost:5000/?productPage=2

Figure 7.4 Paging through data

Using these query strings, you can navigate through the catalog of products. There is no way for customers to figure out that these query string parameters exist, and even if there were, customers are not going to want to navigate this way. Instead, I need to render some page links at the bottom of each list of products so that customers can navigate between pages. To do this, I am going to create a tag helper, which generates the HTML markup for the links I require.

Adding the view model

To support the tag helper, I am going to pass information to the view about the number of pages available, the current page, and the total number of products in the repository. The easiest way to do this is to create a view model class, which is used specifically to pass data between a controller and a view. Create a Models/ViewModels folder in the SportsStore project, add to it a class file named PagingInfo.cs, and define the class shown in listing 7.27.

Listing 7.27 The contents of the PagingInfo.cs file in the SportsStore/Models/ViewModels folder

namespace SportsStore.Models.ViewModels {

    public class PagingInfo {
        public int TotalItems { get; set; }
        public int ItemsPerPage { get; set; }
        public int CurrentPage { get; set; }

        public int TotalPages => 
            (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
    }
}

Adding the tag helper class

Now that I have a view model, it is time to create a tag helper class. Create a folder named Infrastructure in the SportsStore project and add to it a class file called PageLinkTagHelper.cs, with the code shown in listing 7.28. Tag helpers are a big part of ASP.NET Core development, and I explain how they work and how to use and create them in chapters 25–27.

TIP The Infrastructure folder is where I put classes that deliver the plumbing for an application but that are not related to the application’s main functionality. You don’t have to follow this convention in your own projects.

Listing 7.28 The contents of the PageLinkTagHelper.cs file in the SportsStore/Infrastructure folder

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SportsStore.Models.ViewModels;

namespace SportsStore.Infrastructure {

    [HtmlTargetElement("div", Attributes = "page-model")]
    public class PageLinkTagHelper : TagHelper {
        private IUrlHelperFactory urlHelperFactory;

        public PageLinkTagHelper(IUrlHelperFactory helperFactory) {
            urlHelperFactory = helperFactory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext? ViewContext { get; set; }

        public PagingInfo? PageModel { get; set; }

        public string? PageAction { get; set; }

        public override void Process(TagHelperContext context,
                TagHelperOutput output) {
            if (ViewContext != null && PageModel != null) {
                IUrlHelper urlHelper 
                    = urlHelperFactory.GetUrlHelper(ViewContext);
                TagBuilder result = new TagBuilder("div");
                for (int i = 1; i <= PageModel.TotalPages; i++) {
                    TagBuilder tag = new TagBuilder("a");
                    tag.Attributes["href"] = urlHelper.Action(PageAction,
                       new { productPage = i });
                    tag.InnerHtml.Append(i.ToString());
                    result.InnerHtml.AppendHtml(tag);
                }
                output.Content.AppendHtml(result.InnerHtml);
            }
        }
    }
}

This tag helper populates a div element with a elements that correspond to pages of products. I am not going to go into detail about tag helpers now; it is enough to know that they are one of the most useful ways that you can introduce C# logic into your views. The code for a tag helper can look tortured because C# and HTML don’t mix easily. But using tag helpers is preferable to including blocks of C# code in a view because a tag helper can be easily unit tested.

Most ASP.NET Core components, such as controllers and views, are discovered automatically, but tag helpers have to be registered. In listing 7.29, I have added a statement to the _ViewImports.cshtml file in the Views folder that tells ASP.NET Core to look for tag helper classes in the SportsStore project. I also added an @using expression so that I can refer to the view model classes in views without having to qualify their names with the namespace.

Listing 7.29 Registering a tag helper in the _ViewImports.cshtml file in the SportsStore/Views folder

@using SportsStore.Models
@using SportsStore.Models.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, SportsStore

Unit test: creating page links

To test the PageLinkTagHelper tag helper class, I call the Process method with test data and provide a TagHelperOutput object that I inspect to see the HTML that is generated, as follows, which I defined in a new PageLinkTagHelperTests.cs file in the SportsStore.Tests project:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Moq;
using SportsStore.Infrastructure;
using SportsStore.Models.ViewModels;
using Xunit;

namespace SportsStore.Tests {

    public class PageLinkTagHelperTests {

        [Fact]
        public void Can_Generate_Page_Links() {
            // Arrange
            var urlHelper = new Mock<IUrlHelper>();
            urlHelper.SetupSequence(x => 
                     x.Action(It.IsAny<UrlActionContext>()))
                .Returns("Test/Page1")
                .Returns("Test/Page2")
                .Returns("Test/Page3");

            var urlHelperFactory = new Mock<IUrlHelperFactory>();
            urlHelperFactory.Setup(f =>
                    f.GetUrlHelper(It.IsAny<ActionContext>()))
                        .Returns(urlHelper.Object);

            var viewContext = new Mock<ViewContext>();

            PageLinkTagHelper helper =
                    new PageLinkTagHelper(urlHelperFactory.Object) {
                        PageModel = new PagingInfo {
                            CurrentPage = 2,
                            TotalItems = 28,
                            ItemsPerPage = 10
                        },
                        ViewContext = viewContext.Object,
                        PageAction = "Test"
                    };

            TagHelperContext ctx = new TagHelperContext(
                new TagHelperAttributeList(),
                new Dictionary<object, object>(), "");

            var content = new Mock<TagHelperContent>();
            TagHelperOutput output = new TagHelperOutput("div",
                new TagHelperAttributeList(),
                (cache, encoder) => Task.FromResult(content.Object));

            // Act
            helper.Process(ctx, output);

            // Assert
            Assert.Equal(@"<a href=""Test/Page1"">1</a>"
                + @"<a href=""Test/Page2"">2</a>"
                + @"<a href=""Test/Page3"">3</a>",
                 output.Content.GetContent());
        }
    }
}

The complexity in this test is in creating the objects that are required to create and use a tag helper. Tag helpers use IUrlHelperFactory objects to generate URLs that target different parts of the application, and I have used Moq to create an implementation of this interface and the related IUrlHelper interface that provides test data.

The core part of the test verifies the tag helper output by using a literal string value that contains double quotes. C# is perfectly capable of working with such strings, as long as the string is prefixed with @ and uses two sets of double quotes ("") in place of one set of double quotes. You must remember not to break the literal string into separate lines unless the string you are comparing to is similarly broken. For example, the literal I use in the test method has wrapped onto several lines because the width of a printed page is narrow. I have not added a newline character; if I did, the test would fail.

Adding the view model data

I am not quite ready to use the tag helper because I have yet to provide an instance of the PagingInfo view model class to the view. To do this, I added a class file called ProductsListViewModel.cs to the Models/ViewModels folder of the SportsStore project with the content shown in listing 7.30.

Listing 7.30 The contents of the ProductsListViewModel.cs file in the SportsStore/Models/ViewModels folder

namespace SportsStore.Models.ViewModels {

    public class ProductsListViewModel {
        public IEnumerable<Product> Products { get; set; } 
            = Enumerable.Empty<Product>();
        public PagingInfo PagingInfo { get; set; } = new();
    }
}

I can update the Index action method in the HomeController class to use the ProductsListViewModel class to provide the view with details of the products to display on the page and with details of the pagination, as shown in listing 7.31.

Listing 7.31 Updating the action method in the HomeController.cs file in the SportsStore/Controllers folder

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using SportsStore.Models.ViewModels;

namespace SportsStore.Controllers {
    public class HomeController : Controller {
        private IStoreRepository repository;
        public int PageSize = 4;

        public HomeController(IStoreRepository repo) {
            repository = repo;
        }

        public ViewResult Index(int productPage = 1)
           => View(new ProductsListViewModel {
               Products = repository.Products
                   .OrderBy(p => p.ProductID)
                   .Skip((productPage - 1) * PageSize)
                   .Take(PageSize),
               PagingInfo = new PagingInfo {
                   CurrentPage = productPage,
                   ItemsPerPage = PageSize,
                   TotalItems = repository.Products.Count()
               }
           });
    }
}

These changes pass a ProductsListViewModel object as the model data to the view.

Unit test: page model view data

I need to ensure that the controller sends the correct pagination data to the view. Here is the unit test I added to the HomeControllerTests class in the test project to make sure:

...
[Fact]
public void Can_Send_Pagination_View_Model() {

    // Arrange
    Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
    mock.Setup(m => m.Products).Returns((new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
        new Product {ProductID = 4, Name = "P4"},
        new Product {ProductID = 5, Name = "P5"}
    }).AsQueryable<Product>());

    // Arrange
    HomeController controller =
        new HomeController(mock.Object) { PageSize = 3 };

    // Act
    ProductsListViewModel result =
        controller.Index(2)?.ViewData.Model as ProductsListViewModel 
            ?? new();

    // Assert
    PagingInfo pageInfo = result.PagingInfo;
    Assert.Equal(2, pageInfo.CurrentPage);
    Assert.Equal(3, pageInfo.ItemsPerPage);
    Assert.Equal(5, pageInfo.TotalItems);
    Assert.Equal(2, pageInfo.TotalPages);
} 
...

I also need to modify the earlier unit tests to reflect the new result from the Index action method. Here are the revised tests:

...
[Fact]
public void Can_Use_Repository() {
    // Arrange
    Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
    mock.Setup(m => m.Products).Returns((new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"}
    }).AsQueryable<Product>());

    HomeController controller = new HomeController(mock.Object);

    // Act
    ProductsListViewModel result =
        controller.Index()?.ViewData.Model as ProductsListViewModel 
            ?? new();

    // Assert
    Product[] prodArray = result.Products.ToArray();
    Assert.True(prodArray.Length == 2);
    Assert.Equal("P1", prodArray[0].Name);
    Assert.Equal("P2", prodArray[1].Name);
}

[Fact]
public void Can_Paginate() {
    // Arrange
    Mock<IStoreRepository> mock = new Mock<IStoreRepository>();
    mock.Setup(m => m.Products).Returns((new Product[] {
        new Product {ProductID = 1, Name = "P1"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
        new Product {ProductID = 4, Name = "P4"},
        new Product {ProductID = 5, Name = "P5"}
    }).AsQueryable<Product>());

    HomeController controller = new HomeController(mock.Object);
    controller.PageSize = 3;

    // Act
    ProductsListViewModel result =
        controller.Index(2)?.ViewData.Model as ProductsListViewModel 
            ?? new();

    // Assert
    Product[] prodArray = result.Products.ToArray();
    Assert.True(prodArray.Length == 2);
    Assert.Equal("P4", prodArray[0].Name);
    Assert.Equal("P5", prodArray[1].Name);
}
...

I would usually create a common setup method, given the degree of duplication between these two test methods. However, since I am delivering the unit tests in individual sidebars like this one, I am going to keep everything separate so you can see each test on its own.

The view is currently expecting a sequence of Product objects, so I need to update the Index.cshtml file, as shown in listing 7.32, to deal with the new view model type.

Listing 7.32 Updating the Index.cshtml file in the SportsStore/Views/Home folder

@model ProductsListViewModel

@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
    <div>
        <h3>@p.Name</h3>
        @p.Description
        <h4>@p.Price.ToString("c")</h4>
    </div>
}

I have changed the @model directive to tell Razor that I am now working with a different data type. I updated the foreach loop so that the data source is the Products property of the model data.

Displaying the page links

I have everything in place to add the page links to the Index view. I created the view model that contains the paging information, updated the controller so that it passes this information to the view, and changed the @model directive to match the new model view type. All that remains is to add an HTML element that the tag helper will process to create the page links, as shown in listing 7.33.

Listing 7.33 Adding the pagination links in the Index.cshtml file in the SportsStore/Views/Home folder

@model ProductsListViewModel

@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
    <div>
        <h3>@p.Name</h3>
        @p.Description
        <h4>@p.Price.ToString("c")</h4>
    </div>
}

<div page-model="@Model.PagingInfo" page-action="Index"></div>

Restart ASP.NET Core and request http://localhost:5000, and you will see the new page links, as shown in figure 7.5. The style is still basic, which I will fix later in the chapter. What is important for the moment is that the links take the user from page to page in the catalog and allow for exploration of the products for sale. When Razor finds the page-model attribute on the div element, it asks the PageLinkTagHelper class to transform the element, which produces the set of links shown in the figure.

Figure 7.5 Displaying page navigation links

7.4.2 Improving the URLs

I have the page links working, but they still use the query string to pass page information to the server, like this:
http://localhost/?productPage=2

I can create URLs that are more appealing by creating a scheme that follows the pattern of composable URLs. A composable URL is one that makes sense to the user, like this one:
http://localhost/Page2

The ASP.NET Core routing feature makes it easy to change the URL scheme in an application. All I need to do is add a new route in the Program.cs file, as shown in listing 7.34.

Listing 7.34 Adding a new route in the Program.cs file in the SportsStore folder

using Microsoft.EntityFrameworkCore;
using SportsStore.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<StoreDbContext>(opts => {
    opts.UseSqlServer(
        builder.Configuration["ConnectionStrings:SportsStoreConnection"]);
});

builder.Services.AddScoped<IStoreRepository, EFStoreRepository>();

var app = builder.Build();

app.UseStaticFiles();
app.MapControllerRoute("pagination",
    "Products/Page{productPage}",
    new { Controller = "Home", action = "Index" });
app.MapDefaultControllerRoute();

SeedData.EnsurePopulated(app);

app.Run();

This is the only alteration required to change the URL scheme for product pagination. ASP.NET Core and the routing function are tightly integrated, so the application automatically reflects a change like this in the URLs used by the application, including those generated by tag helpers like the one I use to generate the page navigation links.

Restart ASP.NET Core, request http://localhost:5000, and click one of the pagination links. The browser will navigate to a URL that uses the new URL scheme, as shown in figure 7.6.

Figure 7.6 The new URL scheme displayed in the browser

7.5 Styling the content

I have built a great deal of infrastructure, and the basic features of the application are starting to come together, but I have not paid any attention to appearance. Even though this book is not about design or CSS, the SportsStore application design is so miserably plain that it undermines its technical strengths. In this section, I will put some of that right. I am going to implement a classic two-column layout with a header, as shown in figure 7.7.

Figure 7.7 The design goal for the SportsStore application

7.5.1 Installing the Bootstrap package

I am going to use the Bootstrap package to provide the CSS styles I will apply to the application. As explained in chapter 4, client-side packages are installed using LibMan. If you did not install the LibMan package when following the examples in chapter 4, use a PowerShell command prompt to run the commands shown in listing 7.35, which remove any existing LibMan package and install the version required for this book.

Listing 7.35 Installing the LibMan tool package

dotnet tool uninstall --global Microsoft.Web.LibraryManager.Cli
dotnet tool install --global Microsoft.Web.LibraryManager.Cli --version 2.1.175

Once you have installed LibMan, run the commands shown in listing 7.36 in the SportsStore folder to initialize the example project and install the Bootstrap package.

Listing 7.36 Initializing the example project

libman init -p cdnjs
libman install bootstrap@5.2.3 -d wwwroot/lib/bootstrap

7.5.2 Applying Bootstrap styles

Razor layouts provide common content so that it doesn’t have to be repeated in multiple views. Add the elements shown in listing 7.37 to the _Layout.cshtml file in the Views/Shared folder to include the Bootstrap CSS stylesheet in the content sent to the browser and define a common header that will be used throughout the SportsStore application.

Listing 7.37 Applying Bootstrap CSS to the _Layout.cshtml file in the SportsStore/Views/Shared folder

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>SportsStore</title>
    <link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />    
</head>
<body>
    <div class="bg-dark text-white p-2">
        <span class="navbar-brand ml-2">SPORTS STORE</span>
    </div>
    <div class="row m-1 p-1">
        <div id="categories" class="col-3">
            Put something useful here later
        </div>
        <div class="col-9">
            @RenderBody()
        </div>
    </div>
</body>
</html>

Adding the Bootstrap CSS stylesheet to the layout means that I can use the styles it defines in any of the views that rely on the layout. Listing 7.38 shows the styling I applied to the Index.cshtml file.

Listing 7.38 Styling content in the Index.cshtml file in the SportsStore/Views/Home folder

@model ProductsListViewModel

@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
    <div class="card card-outline-primary m-1 p-1">
        <div class="bg-faded p-1">
            <h4>
                @p.Name
                <span class="badge rounded-pill bg-primary text-white" 
                        style="float:right">
                    <small>@p.Price.ToString("c")</small>
                </span>
            </h4>
        </div>
        <div class="card-text p-1">@p.Description</div>
    </div>
}

<div page-model="@Model.PagingInfo" page-action="Index" 
     page-classes-enabled="true" page-class="btn" 
     page-class-normal="btn-outline-dark"
     page-class-selected="btn-primary" class="btn-group pull-right m-1">
</div>

I need to style the buttons generated by the PageLinkTagHelper class, but I don’t want to hardwire the Bootstrap classes into the C# code because it makes it harder to reuse the tag helper elsewhere in the application or change the appearance of the buttons. Instead, I have defined custom attributes on the div element that specify the classes that I require, and these correspond to properties I added to the tag helper class, which are then used to style the a elements that are produced, as shown in listing 7.39.

Listing 7.39 Adding classes to elements in the PageLinkTagHelper.cs file in the SportsStore/Infrastructure folder

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SportsStore.Models.ViewModels;

namespace SportsStore.Infrastructure {

    [HtmlTargetElement("div", Attributes = "page-model")]
    public class PageLinkTagHelper : TagHelper {
        private IUrlHelperFactory urlHelperFactory;

        public PageLinkTagHelper(IUrlHelperFactory helperFactory) {
            urlHelperFactory = helperFactory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext? ViewContext { get; set; }

        public PagingInfo? PageModel { get; set; }

        public string? PageAction { get; set; }

        public bool PageClassesEnabled { get; set; } = false;
        public string PageClass { get; set; } = String.Empty;
        public string PageClassNormal { get; set; } = String.Empty;
        public string PageClassSelected { get; set; } = String.Empty;

        public override void Process(TagHelperContext context,
                TagHelperOutput output) {
            if (ViewContext != null && PageModel != null) {
                IUrlHelper urlHelper 
                    = urlHelperFactory.GetUrlHelper(ViewContext);
                TagBuilder result = new TagBuilder("div");
                for (int i = 1; i <= PageModel.TotalPages; i++) {
                    TagBuilder tag = new TagBuilder("a");
                    tag.Attributes["href"] = urlHelper.Action(PageAction,
                       new { productPage = i });
                    if (PageClassesEnabled) {
                        tag.AddCssClass(PageClass);
                        tag.AddCssClass(i == PageModel.CurrentPage
                            ? PageClassSelected : PageClassNormal);
                    }
                    tag.InnerHtml.Append(i.ToString());
                    result.InnerHtml.AppendHtml(tag);
                }
                output.Content.AppendHtml(result.InnerHtml);
            }
        }
    }
}

The values of the attributes are automatically used to set the tag helper property values, with the mapping between the HTML attribute name format (page-class-normal) and the C# property name format (PageClassNormal) taken into account. This allows tag helpers to respond differently based on the attributes of an HTML element, creating a more flexible way to generate content in an ASP.NET Core application.

Restart ASP.NET Core and request http://localhost:5000, and you will see the appearance of the application has been improved—at least a little, anyway—as illustrated by figure 7.8.

Figure 7.8 Applying styles to the SportsStore application

7.5.3 Creating a partial view

As a finishing flourish for this chapter, I am going to refactor the application to simplify the Index.cshtml view. I am going to create a partial view, which is a fragment of content that you can embed into another view, rather like a template. I describe partial views in detail in chapter 22, and they help reduce duplication when you need the same content to appear in different places in an application. Rather than copy and paste the same Razor markup into multiple views, you can define it once in a partial view. To create the partial view, I added a Razor View called ProductSummary.cshtml to the Views/Shared folder and added the markup shown in listing 7.40.

Listing 7.40 The contents of the ProductSummary.cshtml file in the SportsStore/Views/Shared folder

@model Product

<div class="card card-outline-primary m-1 p-1">
    <div class="bg-faded p-1">
        <h4>
            @Model.Name
            <span class="badge rounded-pill bg-primary text-white"
                  style="float:right">
                <small>@Model.Price.ToString("c")</small>
            </span>
        </h4>
    </div>
    <div class="card-text p-1">@Model.Description</div>
</div>

Now I need to update the Index.cshtml file in the Views/Home folder so that it uses the partial view, as shown in listing 7.41.

Listing 7.41 Using a partial view in the Index.cshtml file in the SportsStore/Views/Home folder

@model ProductsListViewModel

@foreach (var p in Model.Products ?? Enumerable.Empty<Product>()) {
    <partial name="ProductSummary" model="p" />
}

<div page-model="@Model.PagingInfo" page-action="Index"
     page-classes-enabled="true" page-class="btn"
     page-class-normal="btn-outline-dark"
     page-class-selected="btn-primary" class="btn-group pull-right m-1">
</div>

I have taken the markup that was previously in the @foreach expression in the Index.cshtml view and moved it to the new partial view. I call the partial view using a partial element, using the name and model attributes to specify the name of the partial view and its view model. Using a partial view allows the same markup to be inserted into any view that needs to display a summary of a product.

Restart ASP.NET Core and request http://localhost:5000, and you will see that introducing the partial view doesn’t change the appearance of the application; it just changes where Razor finds the content that is used to generate the response sent to the browser.

Summary

• The SportsStore ASP.NET Core project is created using the basic ASP.NET Core template.

• ASP.NET Core has close integration with Entity Framework Core, which is the .NET framework for working with relational data.

• Data can be paginated by including the page number in the request, either using the query string or the URL path and using the page when querying the database.

• The HTML content generated by ASP.NET Core can be styled using popular CSS frameworks, such as Bootstrap.

Pro ASP.NET Core 7 Chapter 5 Essential C# features

5 Essential C# features

This chapter covers

• Using C# language features for ASP.NET Core development
• Dealing with null values and the null state analysis feature
• Creating objects concisely
• Adding features to classes without directly modifying them
• Expressing functions concisely
• Modifying interfaces without breaking implementation classes
• Defining asynchronous methods

In this chapter, I describe C# features used in web application development that are not widely understood or that often cause confusion. This is not a book about C#, however, so I provide only a brief example for each feature so that you can follow the examples in the rest of the book and take advantage of these features in your projects. Table 5.1 provides a guide to this chapter.

Table 5.1 Chapter guide

Problem Solution Listing
Reducing duplication in using statements Use global or implicit using statements. 8–10
Managing null values Use nullable and non-nullable types, which are managed with the null management operators. 11–20
Mixing static and dynamic values in strings Use string interpolation. 21
Initializing and populate objects Use the object and collection initializers and target-typed new expressions. 22–26
Assigning a value for specific types Use pattern matching. 27, 28
Extending the functionality of a class without modifying it Define an extension method. 29–36
Expressing functions and methods concisely Use lambda expressions. 37–44
Defining a variable without explicitly declaring its type Use the var keyword. 45–47
Modifying an interface without requiring changes in its implementation classes Define a default implementation. 48–52
Performing work asynchronously Use tasks or the async/await keywords. 53–55
Producing a sequence of values over time Use an asynchronous enumerable. 56–59
Getting the name of a class or member Use a nameof expression. 60, 61

5.1 Preparing for this chapter

To create the example project for this chapter, open a new PowerShell command prompt and run the commands shown in listing 5.1. If you are using Visual Studio and prefer not to use the command line, you can create the project using the process described in chapter 4.

TIP You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/manningbooks/pro-asp.net-core-7. See chapter 1 for how to get help if you have problems running the examples.

Listing 5.1 Creating the example project


dotnet new globaljson --sdk-version 7.0.100 --output LanguageFeatures
dotnet new web --no-https --output LanguageFeatures --framework net7.0
dotnet new sln -o LanguageFeatures
dotnet sln LanguageFeatures add LanguageFeatures

5.1.1 Opening the project

If you are using Visual Studio, select File > Open > Project/Solution, select the LanguageFeatures.sln file in the LanguageFeatures folder, and click the Open button to open the solution file and the project it references. If you are using Visual Studio Code, select File > Open Folder, navigate to the LanguageFeatures folder, and click the Select Folder button.

5.1.2 Enabling the MVC Framework

The web project template creates a project that contains a minimal ASP.NET Core configuration. This means the placeholder content that is added by the mvc template used in chapter 3 is not available and that extra steps are required to reach the point where the application can produce useful output. In this section, I make the changes required to set up the MVC Framework, which is one of the application frameworks supported by ASP.NET Core, as I explained in chapter 1. First, to enable the MVC framework, make the changes shown in listing 5.2 to the Program.cs file.

Listing 5.2 Enabling MVC in the Program.cs file in the LanguageFeatures folder


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

//app.MapGet("/", () => "Hello World!");
app.MapDefaultControllerRoute();

app.Run();

I explain how to configure ASP.NET Core applications in part 2, but the two statements added in listing 5.2 provide a basic MVC framework setup using a default configuration.

5.1.3 Creating the application components

Now that the MVC framework is set up, I can add the application components that I will use to demonstrate important C# language features. As you create these components, you will see that the code editor underlines some expressions to warn you of potential problems. These are safe to ignore until the “Understanding Null State Analysis” section, where I explain their significance.

Creating the data model

I started by creating a simple model class so that I can have some data to work with. I added a folder called Models and created a class file called Product.cs within it, which I used to define the class shown in listing 5.3.

Listing 5.3 The contents of the Product.cs file in the Models folder


namespace LanguageFeatures.Models {
    public class Product {

        public string Name { get; set; }
        public decimal? Price { get; set; }

        public static Product[] GetProducts() {

            Product kayak = new Product { 
                Name = "Kayak", Price = 275M 
            };

            Product lifejacket = new Product { 
                Name = "Lifejacket", Price = 48.95M 
            };

            return new Product[] { kayak, lifejacket, null };
        }
    }
}

The Product class defines Name and Price properties, and there is a static method called GetProducts that returns a Product array. One of the elements contained in the array returned by the GetProducts method is set to null, which I will use to demonstrate some useful language features later in the chapter.

The Visual Studio and Visual Studio Code editors will highlight a problem with the Name property. This is a deliberate error that I explain later in the chapter and which should be ignored for now.

Creating the controller and view

For the examples in this chapter, I use a simple controller class to demonstrate different language features. I created a Controllers folder and added to it a class file called HomeController.cs, the contents of which are shown in listing 5.4.

Listing 5.4 The contents of the HomeController.cs file in the Controllers folder


using Microsoft.AspNetCore.Mvc;

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            return View(new string[] { "C#", "Language", "Features" });
        }
    }
}

The Index action method tells ASP.NET Core to render the default view and provides it with an array of strings as its view model, which will be included in the HTML sent to the client. To create the view, I added a Views/Home folder (by creating a Views folder and then adding a Home folder within it) and added a Razor View called Index.cshtml, the contents of which are shown in listing 5.5.

Listing 5.5 The contents of the Index.cshtml file in the Views/Home folder


@model IEnumerable<string>
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Language Features</title>
</head>
<body>
    <ul>
        @foreach (string s in Model) {
            <li>@s</li>
        }
    </ul>
</body>
</html>

The code editor will highlight part of this file to denote a warning, which I explain shortly.

5.1.4 Selecting the HTTP port

Change the HTTP port that ASP.NET Core uses to receive requests, as shown in listing 5.6.

Listing 5.6 Setting the HTTP port in the launchSettings.json file in the Properties folder


{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 0
    }
  },
  "profiles": {
    "LanguageFeatures": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

5.1.5 Running the example application

Start ASP.NET Core by running the command shown in listing 5.7 in the LanguageFeatures folder.

Listing 5.7 Running the example application


dotnet run

The output from the dotnet run command will include two build warnings, which I explain in the “Understanding Null State Analysis” section. Once ASP.NET Core has started, use a web browser to request http://localhost:5000, and you will see the output shown in figure 5.1.

Figure 5.1 Running the example application

Since the output from all the examples in this chapter is text, I will show the messages displayed by the browser like this:
• C#
• Language
• Features

5.2 Understanding top-level statements

Top-level statements are intended to remove unnecessary code structure from class files. A project can contain one file that defines code statements outside of a namespace or a file. For ASP.NET Core applications, this feature is used to configure the application in the Program.cs file. Here is the content of the Program.cs file in the example application for this chapter:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

//app.MapGet("/", () => "Hello World!");

app.MapDefaultControllerRoute();

app.Run();

If you have used earlier versions of ASP.NET Core, you will be familiar with the Startup class, which was used to configure the application. Top-level statements have allowed this process to be simplified, and all of the configuration statements are now defined in the Program.cs file.

The compiler will report an error if there is more than one file in a project with top-level statements, so the Program.cs file is the only place you will find them in an ASP.NET Core project.

5.3 Understanding global using statements

C# supports global using statements, which allow a using statement to be defined once but take effect throughout a project. Traditionally, each code file contains a series of using statements that declare dependencies on the namespaces that it requires. Listing 5.8 adds a using statement that provides access to the types defined in the Models namespace. (The code editor will highlight part of this code listing, which I explain in the “Understanding Null State Analysis” section.)

Listing 5.8 Adding a statement in the HomeController.cs file in the Controllers folder


using Microsoft.AspNetCore.Mvc;
using LanguageFeatures.Models;

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product[] products = Product.GetProducts();
            return View(new string[] { products[0].Name });
        }
    }
}

To access the Product class, I added a using statement for the namespace that contains it, which is LanguageFeatures.Models. The code file already contains a using statement for the Microsoft.AspNetCore.Mvc namespace, which provides access to the Controller class, from which the HomeController class is derived.

In most projects, some namespaces are required throughout the application, such as those containing data model classes. This can result in a long list of using statements, duplicated in every code file. Global using statements address this problem by allowing using statements for commonly required namespaces to be defined in a single location. Add a code file named GlobalUsings.cs to the LanguageFeatures project with the content shown in listing 5.9.

Listing 5.9 The contents of the GlobalUsings.cs file in the LanguageFeatures folder


global using LanguageFeatures.Models;
global using Microsoft.AspNetCore.Mvc;

The global keyword is used to denote a global using. The statements in listing 5.9 make the LanguageFeatures.Models and Microsoft.AspNetCore.Mvc namespaces available throughout the application, which means they can be removed from the HomeController.cs file, as shown in listing 5.10.

Listing 5.10 Removing statements in the HomeController.cs file in the Controllers folder


//using Microsoft.AspNetCore.Mvc;
//using LanguageFeatures.Models;

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product[] products = Product.GetProducts();
            return View(new string[] { products[0].Name });
        }
    }
}

If you run the example, you will see the following results displayed in the browser window:
Kayak

You will receive warnings when compiling the project, which I explain in the “Understanding Null State Analysis” section.

NOTE Global using statements are a good idea, but I have not used them in this book because I want to make it obvious when I add a dependency to a new namespace.

5.3.1 Understanding implicit using statements

The ASP.NET Core project templates enable a feature named implicit usings, which define global using statements for these commonly required namespaces:

• System
• System.Collections.Generic
• System.IO
• System.Linq
• System.Net.Http
• System.Net.Http.Json
• System.Threading
• System.Threading.Tasks
• Microsoft.AspNetCore.Builder
• Microsoft.AspNetCore.Hosting
• Microsoft.AspNetCore.Http
• Microsoft.AspNetCore.Routing
• Microsoft.Extensions.Configuration
• Microsoft.Extensions.DependencyInjection
• Microsoft.Extensions.Hosting
• Microsoft.Extensions.Logging

using statements are not required for these namespaces, which are available throughout the application. These namespaces don’t cover all of the ASP.NET Core features, but they do cover the basics, which is why no explicit using statements are required in the Program.cs file.

5.4 Understanding null state analysis

The editor and compiler warnings shown in earlier sections are produced because ASP.NET Core project templates enable null state analysis, in which the compiler identifies attempts to access references that may be unintentionally null, preventing null reference exceptions at runtime.

Open the Product.cs file, and the editor will display two warnings, as shown in figure 5.2. The figure shows how Visual Studio displays a warning, but Visual Studio Code is similar.

Figure 5.2 A null state analysis warning

When null state analysis is enabled, C# variables are divided into two groups: nullable and non-nullable. As their name suggests, nullable variables can be assigned the special value null. This is the behavior that most programmers are familiar with, and it is entirely up to the developer to guard against trying to use null references, which will trigger a NullReferenceException.

By contrast, non-nullable variables can never be null. When you receive a non-nullable variable, you don’t have to guard against a null value because that is not a value that can ever be assigned.

A question mark (the ? character) is appended to a type to denote a nullable type. So, if a variable’s type is string?, for example, then it can be assigned any value string value or null. When attempting to access this variable, you should check to ensure that it isn’t null before attempting to access any of the fields, properties, or methods defined by the string type.

If a variable’s type is string, then it cannot be assigned null values, which means you can confidently access the features it provides without needing to guard against null references.

The compiler examines the code in the project and warns you when it finds statements that might break these rules. The most common issues are attempting to assign null to non-nullable variables and attempting to access members defined by nullable variables without checking to see if they are null. In the sections that follow, I explain the different ways that the warnings raised by the compiler in the example application can be addressed.

NOTE Getting to grips with nullable and non-nullable types can be frustrating. A change in one code file can simply move a warning to another part of the application, and it can feel like you are chasing problems through a project. But it is worth sticking with null state analysis because null reference exceptions are the most common runtime error, and few programmers are disciplined enough to guard against null values without the compiler analysis feature.
5.4.1 Ensuring fields and properties are assigned values

The first warning in the Product.cs file is for the Name field, whose type is string, which is a non-nullable type (because it hasn’t been annotated with a question mark).


...
public string Name { get; set; }
...

One consequence of using non-nullable types is that properties like Name must be assigned a value when a new instance of the enclosing class is created. If this were not the case, then the Name property would not be initialized and would be null. And this is a problem because we can’t assign null to a non-nullable property, even indirectly.

The required keyword can be used to indicate that a value is required for a non-nullable type, as shown in listing 5.11.

Listing 5.11 Using the required keyword in the Product.cs file in the Models folder


namespace LanguageFeatures.Models {
    public class Product {

        public required string Name { get; set; }
        public decimal? Price { get; set; }

        public static Product[] GetProducts() {

            Product kayak = new Product {
                Name = "Kayak", Price = 275M
            };

            Product lifejacket = new Product {
                Name = "Lifejacket", Price = 48.95M
            };

            return new Product[] { kayak, lifejacket, null };
        }
    }
}

The compiler will check to make sure that a value is assigned to the property when a new instance of the containing type is created. The two Product objects used in the listing are created with a value for the Name field, which satisfies the demands of the required keyword. Listing 5.12 omits the Name value from one of Product objects.

Listing 5.12 Omitting a value in the Product.cs file in the Models folder


namespace LanguageFeatures.Models {
    public class Product {

        public required string Name { get; set; }
        public decimal? Price { get; set; }

        public static Product[] GetProducts() {

            Product kayak = new Product {
                Name = "Kayak", Price = 275M
            };

            Product lifejacket = new Product {
                //Name = "Lifejacket", 
                Price = 48.95M
            };

            return new Product[] { kayak, lifejacket, null };
        }
    }
}

If you run the example, the build process will fail with this error:

Required member 'Product.Name' must be set in the object initializer or attribute constructor.

This error—and the corresponding red line in the code editor—tell you that a value for the Name property is required but has not been provided.

5.4.2 Providing a default value for non-nullable types

The required keyword is a good way to denote a property that cannot be null, and which requires a value when an object is created. This approach can become cumbersome in situations where there may not always be a suitable data value available, because it requires the code wants to create the object to provide a fallback value and there is no good way to enforce consistency.

For these situations a default value can be used instead of the required keyword, as shown in listing 5.13.

Listing 5.13 Providing a default value in the Product.cs file in the Models folder


namespace LanguageFeatures.Models {
    public class Product {

        public string Name { get; set; } = string.Empty;
        public decimal? Price { get; set; }

        public static Product[] GetProducts() {

            Product kayak = new Product {
                Name = "Kayak", Price = 275M
            };

            Product lifejacket = new Product {
                //Name = "Lifejacket", 
                Price = 48.95M
            };

            return new Product[] { kayak, lifejacket, null };
        }
    }
}

The default value in this example is the empty string. This value will be replaced for Product objects that are created with a Name value and ensures consistency for objects that are created without one.

5.4.3 Using nullable types

The remaining warning in the Product.cs file occurs because there is a mismatch between the type used for the result of the GetProducts method and the values that are used to initialize it:


...
return new Product[] { kayak, lifejacket, null };
...

The type of the array that is created is Product[], which contains non-nullable Product references. But one of the values used to populate the array is null, which isn’t allowed. Listing 5.14 changes the array type so that nullable values are allowed.

Listing 5.14 Using a nullable type in the Product.cs file in the Models folder


namespace LanguageFeatures.Models {
    public class Product {

        public string Name { get; set; } = string.Empty;
        public decimal? Price { get; set; }

        public static Product?[] GetProducts() {

            Product kayak = new Product {
                Name = "Kayak", Price = 275M
            };

            Product lifejacket = new Product {
                //Name = "Lifejacket", 
                Price = 48.95M
            };

            return new Product?[] { kayak, lifejacket, null };
        }
    }
}
/

The type Product?[] denotes an array of Product? references, which means the result can include null. Notice that I had to make the same change to the result type declared by the GetProducts method because a Product?[] array cannot be used where a Product[] is expected.

Selecting the right nullable type

Care must be taken to apply the question mark correctly, especially when dealing with arrays and collections. A variable of type Product?[] denotes an array that can contain Product or null values but that won’t be null itself:


...
Product?[] arr1 = new Product?[] { kayak, lifejacket, null };  // OK
Product?[] arr2 = null;                                        // Not OK
...

A variable of type Product[]? is an array that can hold only Product values and not null values, but the array itself may be null:


...
Product[]? arr1 = new Product?[] { kayak, lifejacket, null }; // Not OK
Product[]? arr2 = null;                                       // OK
...

A variable of type Product?[]? is an array that can contain Product or null values and that can itself be null:


...
Product?[]? arr1 = new Product?[] { kayak, lifejacket, null }; // OK
Product?[]? arr2 = null;                                          // Also OK
...

Null state analysis is a useful feature, but that doesn’t mean it is always easy to understand.

5.4.4 Checking for null values

I explained that dealing with null state analysis warnings can feel like chasing a problem through code, and you can see a simple example of this in the HomeController.cs file in the Controllers folder. In listing 5.14, I changed the type returned by the GetProducts method to allow null values, but that has created a mismatch in the HomeController class, which invokes that method and assigns the result to an array of non-nullable Product values:


...
Product[] products = Product.GetProducts();
...

This is easily resolved by changing the type of the products variable to match the type returned by the GetProducts method, as shown in listing 5.15.

Listing 5.15 Changing Type in the HomeController.cs File in the Controllers Folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();
            return View(new string[] { products[0].Name });
        }
    }
}

This resolves one warning and introduces another, as shown in figure 5.3.

Figure 5.3 An additional null state analysis warning

The statement flagged by the compiler attempts to access the Name field of the element at index zero in the array, which might be null since the array type is Product?[]. Addressing this issue requires a check for null values, as shown in listing 5.16.

Listing 5.16 Guarding against null in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();
            Product? p = products[0];
            string val;
            if (p != null) {
                val = p.Name;
            } else {
                val = "No value";
            }
            return View(new string[] { val });
        }
    }
}

This is an especially verbose way of avoiding a null, which I will refine shortly. But it demonstrates an important point, which is that the compiler can understand the effect of C# expressions when checking for a null reference. In listing 5.16, I use an if statement to see if a Product? variable is not null, and the compiler understands that the variable cannot be null within the scope of the if clause and doesn’t generate a warning when I read the name field:


...
if (p != null) {
    val = p.Name;
} else {
    val = "No value";
}
...

The compiler has a sophisticated understanding of C# but doesn’t always get it right, and I explain what to do when the compiler isn’t able to accurately determine whether a variable is null in the “Overriding Null State Analysis” section.

Using the null conditional operator

The null conditional operator is a more concise way of avoiding member access for null values, as shown in listing 5.17.

Listing 5.17 Null conditional operator in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {

            Product?[] products = Product.GetProducts();

            string? val = products[0]?.Name;
            if (val != null) {
                return View(new string[] { val });
            }
            return View(new string[] { "No Value" });
        }
    }
}

The null conditional operator is a question mark applied before a member is accessed, like this:


...
string? val = products[0]?.Name;
...

The operator returns null if it is applied to a variable that is null. In this case, if the element at index zero of the products array is null, then the operator will return null and prevent an attempt to access the Name property, which would cause an exception. If products[0] isn’t null, then the operator does nothing, and the expression returns the value assigned to the Name property. Applying the null conditional operator can return null, and its result must always be assigned to a nullable variable, such as the string? used in this example.

Using the null-coalescing operator

The null-coalescing operator is two question mark characters (??) and is used to provide a fallback value, often used in conjunction with the null conditional operator, as shown in listing 5.18.

Listing 5.18 Using the null-coalescing operator in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();
            return View(new string[] { products[0]?.Name ??"No Value" });
        }
    }
}

The ?? operator returns the value of its left-hand operand if it isn’t null. If the left-hand operand is null, then the ?? operator returns the value of its right-hand operand. This behavior works well with the null conditional operator. If products[0] is null, then the ? operator will return null, and the ?? operator will return "No Value". If products[0] isn’t null, then the result will be the value of its Name property. This is a more concise way of performing the same null checks shown in earlier examples.

NOTE The ? and ?? operators cannot always be used, and you will see examples in later chapters where I use an if statement to check for null values. One common example is when using the await/async keywords, which are described later in this chapter, and which do not integrate well with the null conditional operator.

5.4.5 Overriding null state analysis

The C# compiler has a sophisticated understanding of when a variable can be null, but it doesn’t always get it right, and there are times when you have a better understanding of whether a null value can arise than the compiler. In these situations, the null-forgiving operator can be used to tell the compiler that a variable isn’t null, regardless of what the null state analysis suggests, as shown in listing 5.19.

Listing 5.19 Using the null-forgiving operator in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();
            return View(new string[] { products[0]!.Name });
        }
    }
}

The null-forgiving operator is an exclamation mark and is used in this example to tell the compiler that products[0] isn’t null, even though null state analysis has identified that it might be.

When using the ! operator, you are telling the compiler that you have better insight into whether a variable can be null, and, naturally, this should be done only when you are entirely confident that you are right.

5.4.6 Disabling null state analysis warnings

An alternative to the null-forgiving operator is to disable null state analysis warnings for a particular section of code or a complete code file, as shown in listing 5.20.

Listing 5.20 Disabling warnings in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();
            #pragma warning disable CS8602
            return View(new string[] { products[0].Name });
        }
    }
}

This listing uses a #pragma directive to suppress warning CS8602 (you can identify warnings in the output from the build process).

NOTE .NET includes a set of advanced attributes that can be used to provide the compiler with guidance for null state analysis. These are not widely used and are encountered only in chapter 36 of this book because they are used by one part of the ASP.NET Core API. See https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis for details.

5.5 Using string interpolation

C# supports string interpolation to create formatted strings, which uses templates with variable names that are resolved and inserted into the output, as shown in listing 5.21.

Listing 5.21 Using string interpolation in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            Product?[] products = Product.GetProducts();

            return View(new string[] { 
                $"Name: {products[0]?.Name}, Price: { products[0]?.Price }"
            });
        }
    }
}

Interpolated strings are prefixed with the $ character and contain holes, which are references to values contained within the { and } characters. When the string is evaluated, the holes are filled in with the current values of the variables or constants that are specified.

TIP String interpolation supports the string format specifiers, which can be applied within holes, so $"Price: {price:C2}" would format the price value as a currency value with two decimal digits, for example.

Start ASP.NET Core and request http://localhost:5000, and you will see a formatted string:


Name: Kayak, Price: 275

5.6 Using object and collection initializers

When I create an object in the static GetProducts method of the Product class, I use an object initializer, which allows me to create an object and specify its property values in a single step, like this:


...
Product kayak = new Product {
    Name = "Kayak", Price = 275M
};
...

This is another syntactic sugar feature that makes C# easier to use. Without this feature, I would have to call the Product constructor and then use the newly created object to set each of the properties, like this:


...
Product kayak = new Product();
kayak.Name = "Kayak";
kayak.Price = 275M;
...

A related feature is the collection initializer, which allows the creation of a collection and its contents to be specified in a single step. Without an initializer, creating a string array, for example, requires the size of the array and the array elements to be specified separately, as shown in listing 5.22.

Listing 5.22 Initializing an object in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            string[] names = new string[3];
            names[0] = "Bob";
            names[1] = "Joe";
            names[2] = "Alice";
            return View("Index", names);
        }
    }
}

Using a collection initializer allows the contents of the array to be specified as part of the construction, which implicitly provides the compiler with the size of the array, as shown in listing 5.23.

Listing 5.23 A collection initializer in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            return View("Index", new string[] { "Bob", "Joe", "Alice" });
        }
    }
}

The array elements are specified between the { and } characters, which allows for a more concise definition of the collection and makes it possible to define a collection inline within a method call. The code in listing 5.23 has the same effect as the code in listing 5.22. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser window:


Bob
Joe
Alice

5.6.1 Using an index initializer

Recent versions of C# tidy up the way collections that use indexes, such as dictionaries, are initialized. Listing 5.24 shows the Index action rewritten to define a collection using the traditional C# approach to initializing a dictionary.

Listing 5.24 Initializing a dictionary in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            Dictionary<string, Product> products 
                    = new Dictionary<string, Product> {
                { 
                    "Kayak", 
                    new Product { Name = "Kayak", Price = 275M } 
                },
                { 
                    "Lifejacket",  
                    new Product{ Name = "Lifejacket", Price = 48.95M } 
                }
            };
            return View("Index", products.Keys);
        }
    }
}

The syntax for initializing this type of collection relies too much on the { and } characters, especially when the collection values are created using object initializers. The latest versions of C# support a more natural approach to initializing indexed collections that is consistent with the way that values are retrieved or modified once the collection has been initialized, as shown in listing 5.25.

Listing 5.25 Using collection initializer syntax in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            Dictionary<string, Product> products 
                    = new Dictionary<string, Product> {
                ["Kayak"] = new Product { Name = "Kayak", Price = 275M },
                ["Lifejacket"] = new Product { Name = "Lifejacket", 
                    Price = 48.95M }
            };
            return View("Index", products.Keys);
        }
    }
}

The effect is the same—to create a dictionary whose keys are Kayak and Lifejacket and whose values are Product objects—but the elements are created using the index notation that is used for other collection operations. Restart ASP.NET Core and request http://localhost:5000, and you will see the following results in the browser:

Kayak
Lifejacket

5.7 Using target-typed new expressions

The example in listing 5.25 is still verbose and declares the collection type when defining the variable and creating an instance with the new keyword:

...
Dictionary<string, Product> products = new Dictionary<string, Product> {
    ["Kayak"] = new Product { Name = "Kayak", Price = 275M },
    ["Lifejacket"] = new Product { Name = "Lifejacket", Price = 48.95M }
};
...

This can be simplified using a target-typed new expression, as shown in listing 5.26.

Listing 5.26 Using a target-typed new expression in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            Dictionary<string, Product> products = new () {
                ["Kayak"] = new Product { Name = "Kayak", Price = 275M },
                ["Lifejacket"] = new Product { Name = "Lifejacket", 
                    Price = 48.95M }
            };
            return View("Index", products.Keys);
        }
    }
}

The type can be replaced with new() when the compiler can determine which type is required and constructor arguments are provided as arguments to the new method. Creating instances with the new() expression works only when the compiler can determine which type is required. Restart ASP.NET Core and request http://localhost:5000, and you will see the following results in the browser:


Kayak
Lifejacket

5.8 Pattern Matching

One of the most useful recent additions to C# is support for pattern matching, which can be used to test that an object is of a specific type or has specific characteristics. This is another form of syntactic sugar, and it can dramatically simplify complex blocks of conditional statements. The is keyword is used to perform a type test, as demonstrated in listing 5.27.

Listing 5.27 Testing a type in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {

            object[] data = new object[] { 275M, 29.95M,
                "apple", "orange", 100, 10 };
            decimal total = 0;
            for (int i = 0; i < data.Length; i++) {
                if (data[i] is decimal d) {
                    total += d;
                }
            }

            return View("Index", new string[] { $"Total: {total:C2}" });
        }
    }
}

The is keyword performs a type check and, if a value is of the specified type, will assign the value to a new variable, like this:

...
if (data[i] is decimal d) {
...

This expression evaluates as true if the value stored in data[i] is a decimal. The value of data[i] will be assigned to the variable d, which allows it to be used in subsequent statements without needing to perform any type conversions. The is keyword will match only the specified type, which means that only two of the values in the data array will be processed (the other items in the array are string and int values). If you run the application, you will see the following output in the browser window:

Total: $304.95

5.8.1 Pattern matching in switch statements

Pattern matching can also be used in switch statements, which support the when keyword for restricting when a value is matched by a case statement, as shown in listing 5.28.

Listing 5.28 Pattern matching in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {

            object[] data = new object[] { 275M, 29.95M,
                "apple", "orange", 100, 10 };
            decimal total = 0;
            for (int i = 0; i < data.Length; i++) {
                switch (data[i]) {
                    case decimal decimalValue:
                        total += decimalValue;
                        break;
                    case int intValue when intValue > 50:
                        total += intValue;
                        break;
                }
            }

            return View("Index", new string[] { $"Total: {total:C2}" });
        }
    }
}

To match any value of a specific type, use the type and variable name in the case statement, like this:


...
case decimal decimalValue:
...

This case statement matches any decimal value and assigns it to a new variable called decimalValue. To be more selective, the when keyword can be included, like this:


...
case int intValue when intValue > 50:
...

This case statement matches int values and assigns them to a variable called intValue, but only when the value is greater than 50. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser window:


Total: $404.95

5.9 Using extension methods

Extension methods are a convenient way of adding methods to classes that you cannot modify directly, typically because they are provided by Microsoft or a third-party package. Listing 5.29 shows the definition of the ShoppingCart class, which I added to the Models folder in a class file called ShoppingCart.cs and which represents a collection of Product objects.

Listing 5.29 The contents of the ShoppingCart.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class ShoppingCart {
        public IEnumerable<Product?>? Products { get; set; }
    }
}

This is a simple class that acts as a wrapper around a sequence of Product objects (I only need a basic class for this example). Note the type of the Products property, which denotes a nullable enumerable of nullable Products, meaning that the Products property may be null and that any sequence of elements assigned to the property may contain null values.

Suppose I need to be able to determine the total value of the Product objects in the ShoppingCart class, but I cannot modify the class because it comes from a third party, and I do not have the source code. I can use an extension method to add the functionality I need.

Add a class file named MyExtensionMethods.cs in the Models folder and use it to define the class shown in listing 5.30.

Listing 5.30 The contents of the MyExtensionMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public static class MyExtensionMethods {

        public static decimal TotalPrices(this ShoppingCart cartParam) {
            decimal total = 0;
            if (cartParam.Products != null) {
                foreach (Product? prod in cartParam.Products) {
                    total += prod?.Price ?? 0;
                }
            }
            return total;
        }
    }
}

Extension methods are static and are defined in static classes. Listing 5.30 defines a single extension method named TotalPrices. The this keyword in front of the first parameter marks TotalPrices as an extension method. The first parameter tells .NET which class the extension method can be applied to—ShoppingCart in this case. I can refer to the instance of the ShoppingCart that the extension method has been applied to by using the cartParam parameter. This extension method enumerates the Product objects in the ShoppingCart and returns the sum of the Product.Price property values. Listing 5.31 shows how I apply the extension method in the Home controller’s action method.

Note Extension methods do not let you break through the access rules that classes define for methods, fields, and properties. You can extend the functionality of a class by using an extension method but only using the class members that you had access to anyway.

Listing 5.31 Applying an extension method in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};
            decimal cartTotal = cart.TotalPrices();
            return View("Index", 
                new string[] { $"Total: {cartTotal:C2}" });
        }
    }
}

The key statement is this one:

...
decimal cartTotal = cart.TotalPrices();
...

I call the TotalPrices method on a ShoppingCart object as though it were part of the ShoppingCart class, even though it is an extension method defined by a different class altogether. .NET will find extension classes if they are in the scope of the current class, meaning that they are part of the same namespace or in a namespace that is the subject of a using statement. Restart ASP.NET Core and request http://localhost:5000, which will produce the following output in the browser window:

Total: $323.95

5.9.1 Applying extension methods to an interface

Extension methods can also be applied to an interface, which allows me to call the extension method on all the classes that implement the interface. Listing 5.32 shows the ShoppingCart class updated to implement the IEnumerable<Product> interface.

Listing 5.32 Implementing an interface in the ShoppingCart.cs file in the Models folder


using System.Collections;

namespace LanguageFeatures.Models {

    public class ShoppingCart : IEnumerable<Product?> {
        public IEnumerable<Product?>? Products { get; set; }

        public IEnumerator<Product?> GetEnumerator() => 
                Products?.GetEnumerator()
                    ?? Enumerable.Empty<Product?>().GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

I can now update the extension method so that it deals with IEnumerable<Product?>, as shown in listing 5.33.

Listing 5.33 Updating an extension method in the MyExtensionMethods.cs file in the Models folder


namespace LanguageFeatures.Models {

    public static class MyExtensionMethods {

        public static decimal TotalPrices(
                this IEnumerable<Product?> products) {
            decimal total = 0;
            foreach (Product? prod in products) {
                total += prod?.Price ?? 0;
            }
            return total;
        }
    }
}

The first parameter type has changed to IEnumerable<Product?>, which means the foreach loop in the method body works directly on Product? objects. The change to using the interface means that I can calculate the total value of the Product objects enumerated by any IEnumerable<Product?>, which includes instances of ShoppingCart but also arrays of Product objects, as shown in listing 5.34.

Listing 5.34 Applying an extension method in the HomeController.cs file in the Controllers folder


namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};

            Product[] productArray = {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M}
            };

            decimal cartTotal = cart.TotalPrices();
            decimal arrayTotal = productArray.TotalPrices();

            return View("Index", new string[] {
                $"Cart Total: {cartTotal:C2}",
                $"Array Total: {arrayTotal:C2}" });
        }
    }
}

Restart ASP.NET Core and request http://localhost:5000, which will produce the following output in the browser, demonstrating that I get the same result from the extension method, irrespective of how the Product objects are collected:


Cart Total: $323.95
Array Total: $323.95 

5.9.2 Creating filtering extension methods

The last thing I want to show you about extension methods is that they can be used to filter collections of objects. An extension method that operates on an IEnumerable<T> and that also returns an IEnumerable<T> can use the yield keyword to apply selection criteria to items in the source data to produce a reduced set of results. Listing 5.35 demonstrates such a method, which I have added to the MyExtensionMethods class.

Listing 5.35 A filtering extension method in the MyExtensionMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public static class MyExtensionMethods {

        public static decimal TotalPrices(
                this IEnumerable<Product?> products) {
            decimal total = 0;
            foreach (Product? prod in products) {
                total += prod?.Price ?? 0;
            }
            return total;
        }

        public static IEnumerable<Product?> FilterByPrice(
                this IEnumerable<Product?> productEnum, 
                decimal minimumPrice) {
            foreach (Product? prod in productEnum) {
                if ((prod?.Price ?? 0) >= minimumPrice) {
                    yield return prod;
                }
            }
        }
    }
}

This extension method, called FilterByPrice, takes an additional parameter that allows me to filter products so that Product objects whose Price property matches or exceeds the parameter are returned in the result. Listing 5.36 shows this method being used.

Listing 5.36 Using the filtering extension method in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};

            Product[] productArray = {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M},
                new Product {Name = "Soccer ball", Price = 19.50M},
                new Product {Name = "Corner flag", Price = 34.95M}
            };

            decimal arrayTotal = 
                productArray.FilterByPrice(20).TotalPrices();

            return View("Index", 
                new string[] { $"Array Total: {arrayTotal:C2}" });
        }
    }
}

When I call the FilterByPrice method on the array of Product objects, only those that cost more than $20 are received by the TotalPrices method and used to calculate the total. If you run the application, you will see the following output in the browser window:

Total: $358.90

5.10 Using lambda expressions

Lambda expressions are a feature that causes a lot of confusion, not least because the feature they simplify is also confusing. To understand the problem that is being solved, consider the FilterByPrice extension method that I defined in the previous section. This method is written so that it can filter Product objects by price, which means I must create a second method if I want to filter by name, as shown in listing 5.37.

Listing 5.37 Adding a filter method in the MyExtensionMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public static class MyExtensionMethods {

        public static decimal TotalPrices(
                this IEnumerable<Product?> products) {
            decimal total = 0;
            foreach (Product? prod in products) {
                total += prod?.Price ?? 0;
            }
            return total;
        }

        public static IEnumerable<Product?> FilterByPrice(
                this IEnumerable<Product?> productEnum, 
                decimal minimumPrice) {
            foreach (Product? prod in productEnum) {
                if ((prod?.Price ?? 0) >= minimumPrice) {
                    yield return prod;
                }
            }
        }

        public static IEnumerable<Product?> FilterByName(
                this IEnumerable<Product?> productEnum, 
                char firstLetter) {

            foreach (Product? prod in productEnum) {
                if (prod?.Name?[0] == firstLetter) {
                    yield return prod;
                }
            }
        }
    }
}

Listing 5.38 shows the use of both filter methods applied in the controller to create two different totals.

Listing 5.38 Using two filter methods in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};

            Product[] productArray = {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M},
                new Product {Name = "Soccer ball", Price = 19.50M},
                new Product {Name = "Corner flag", Price = 34.95M}
            };

            decimal priceFilterTotal = 
                productArray.FilterByPrice(20).TotalPrices();
            decimal nameFilterTotal = 
                productArray.FilterByName('S').TotalPrices();

            return View("Index", new string[] {
                $"Price Total: {priceFilterTotal:C2}",
                $"Name Total: {nameFilterTotal:C2}" });
        }
    }
}

The first filter selects all the products with a price of $20 or more, and the second filter selects products whose name starts with the letter S. You will see the following output in the browser window if you run the example application:

Price Total: $358.90
Name Total: $19.50

5.10.1 Defining functions

I can repeat this process indefinitely to create filter methods for every property and every combination of properties that I am interested in. A more elegant approach is to separate the code that processes the enumeration from the selection criteria. C# makes this easy by allowing functions to be passed around as objects. Listing 5.39 shows a single extension method that filters an enumeration of Product objects but that delegates the decision about which ones are included in the results to a separate function.

Listing 5.39 Creating a general filter method in the MyExtensionMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public static class MyExtensionMethods {

        public static decimal TotalPrices(
                this IEnumerable<Product?> products) {
            decimal total = 0;
            foreach (Product? prod in products) {
                total += prod?.Price ?? 0;
            }
            return total;
        }

        public static IEnumerable<Product?> FilterByPrice(
                this IEnumerable<Product?> productEnum, 
                decimal minimumPrice) {
            foreach (Product? prod in productEnum) {
                if ((prod?.Price ?? 0) >= minimumPrice) {
                    yield return prod;
                }
            }
        }

        public static IEnumerable<Product?> Filter(
                this IEnumerable<Product?> productEnum,
                Func<Product?, bool> selector) {

            foreach (Product? prod in productEnum) {
                if (selector(prod)) {
                    yield return prod;
                }
            }
        }
    }
}

The second argument to the Filter method is a function that accepts a Product? object and that returns a bool value. The Filter method calls the function for each Product? object and includes it in the result if the function returns true. To use the Filter method, I can specify a method or create a stand-alone function, as shown in listing 5.40.

Listing 5.40 Using a function to filter objects in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        bool FilterByPrice(Product? p) {
            return (p?.Price ?? 0) >= 20;
        }

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};

            Product[] productArray = {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M},
                new Product {Name = "Soccer ball", Price = 19.50M},
                new Product {Name = "Corner flag", Price = 34.95M}
            };

            Func<Product?, bool> nameFilter = delegate (Product? prod) {
                return prod?.Name?[0] == 'S';
            };

            decimal priceFilterTotal = productArray
                .Filter(FilterByPrice)
                .TotalPrices();
            decimal nameFilterTotal = productArray
                .Filter(nameFilter)
                .TotalPrices();

            return View("Index", new string[] {
                $"Price Total: {priceFilterTotal:C2}",
                $"Name Total: {nameFilterTotal:C2}" });
        }
    }
}

Neither approach is ideal. Defining methods like FilterByPrice clutters up a class definition. Creating a Func<Product?, bool> object avoids this problem but uses an awkward syntax that is hard to read and hard to maintain. It is this issue that lambda expressions address by allowing functions to be defined in a more elegant and expressive way, as shown in listing 5.41.

Listing 5.41 Using a lambda expression in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        //bool FilterByPrice(Product? p) {
        //    return (p?.Price ?? 0) >= 20;
        //}

        public ViewResult Index() {
            ShoppingCart cart 
                = new ShoppingCart { Products = Product.GetProducts()};

            Product[] productArray = {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M},
                new Product {Name = "Soccer ball", Price = 19.50M},
                new Product {Name = "Corner flag", Price = 34.95M}
            };

            //Func<Product?, bool> nameFilter = delegate (Product? prod) {
            //    return prod?.Name?[0] == 'S';
            //};

            decimal priceFilterTotal = productArray
                .Filter(p => (p?.Price ?? 0) >= 20)
                .TotalPrices();

            decimal nameFilterTotal = productArray
                .Filter(p => p?.Name?[0] == 'S')
                .TotalPrices();

            return View("Index", new string[] {
                $"Price Total: {priceFilterTotal:C2}",
                $"Name Total: {nameFilterTotal:C2}" });
        }
    }
}

The lambda expressions are shown in bold. The parameters are expressed without specifying a type, which will be inferred automatically. The => characters are read aloud as “goes to” and link the parameter to the result of the lambda expression. In my examples, a Product? parameter called p goes to a bool result, which will be true if the Price property is equal or greater than 20 in the first expression or if the Name property starts with S in the second expression. This code works in the same way as the separate method and the function delegate but is more concise and is—for most people—easier to read.

Other Forms for Lambda Expressions

I don’t need to express the logic of my delegate in the lambda expression. I can as easily call a method, like this:

...
prod => EvaluateProduct(prod)
...

If I need a lambda expression for a delegate that has multiple parameters, I must wrap the parameters in parentheses, like this:

...
(prod, count) => prod.Price > 20 && count > 0
...

Finally, if I need logic in the lambda expression that requires more than one statement, I can do so by using braces ({}) and finishing with a return statement, like this:

...
(prod, count) => {
    // ...multiple code statements...
    return result;
}
...

You do not need to use lambda expressions in your code, but they are a neat way of expressing complex functions simply and in a manner that is readable and clear. I like them a lot, and you will see them used throughout this book.

5.10.2 Using lambda expression methods and properties

Lambda expressions can be used to implement constructors, methods, and properties. In ASP.NET Core development, you will often end up with methods that contain a single statement that selects the data to display and the view to render. In listing 5.42, I have rewritten the Index action method so that it follows this common pattern.

Listing 5.42 Creating a common action pattern in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            return View(Product.GetProducts().Select(p => p?.Name));
        }
    }
}

The action method gets a collection of Product objects from the static Product.GetProducts method and uses LINQ to project the values of the Name properties, which are then used as the view model for the default view. If you run the application, you will see the following output displayed in the browser window:

Kayak

There will be empty list items in the browser window as well because the GetProducts method includes a null reference in its results and one of the Product objects is created without a Name value, but that doesn’t matter for this section of the chapter.

When a constructor or method body consists of a single statement, it can be rewritten as a lambda expression, as shown in listing 5.43.

Listing 5.43 A lambda action method in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() => 
            View(Product.GetProducts().Select(p => p?.Name));
    }
}

Lambda expressions for methods omit the return keyword and use => (goes to) to associate the method signature (including its arguments) with its implementation. The Index method shown in listing 5.43 works in the same way as the one shown in listing 5.42 but is expressed more concisely. The same basic approach can also be used to define properties. Listing 5.44 shows the addition of a property that uses a lambda expression to the Product class.

Listing 5.44 A lambda property in the Product.cs file in the Models folder

namespace LanguageFeatures.Models {
    public class Product {

        public string Name { get; set; } = string.Empty;
        public decimal? Price { get; set; }

        public bool NameBeginsWithS =>  Name.Length > 0 && Name[0] == 'S';

        public static Product?[] GetProducts() {
            Product kayak = new Product {
                Name = "Kayak", Price = 275M
            };

            Product lifejacket = new Product {
                //Name = "Lifejacket", 
                Price = 48.95M
            };

            return new Product?[] { kayak, lifejacket, null };
        }
    }
}

5.11 Using type inference and anonymous types

The var keyword allows you to define a local variable without explicitly specifying the variable type, as demonstrated by listing 5.45. This is called type inference, or implicit typing.

Listing 5.45 Using type inference in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            var names = new[] { "Kayak", "Lifejacket", "Soccer ball" };
            return View(names);
        }
    }
}

It is not that the names variable does not have a type; instead, I am asking the compiler to infer the type from the code. The compiler examines the array declaration and works out that it is a string array. Running the example produces the following output:

Kayak
Lifejacket
Soccer ball

5.11.1 Using anonymous types

By combining object initializers and type inference, I can create simple view model objects that are useful for transferring data between a controller and a view without having to define a class or struct, as shown in listing 5.46.

Listing 5.46 An anonymous type in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {
        public ViewResult Index() {

            var products = new[] {
                new { Name = "Kayak", Price = 275M },
                new { Name = "Lifejacket", Price = 48.95M },
                new { Name = "Soccer ball", Price = 19.50M },
                new { Name = "Corner flag", Price = 34.95M }
            };

            return View(products.Select(p => p.Name));
        }
    }
}

Each of the objects in the products array is an anonymously typed object. This does not mean that it is dynamic in the sense that JavaScript variables are dynamic. It just means that the type definition will be created automatically by the compiler. Strong typing is still enforced. You can get and set only the properties that have been defined in the initializer, for example. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser window:

Kayak
Lifejacket
Soccer ball
Corner flag

The C# compiler generates the class based on the name and type of the parameters in the initializer. Two anonymously typed objects that have the same property names and types defined in the same order will be assigned to the same automatically generated class. This means that all the objects in the products array will have the same type because they define the same properties.

TIP I have to use the var keyword to define the array of anonymously typed objects because the type isn’t created until the code is compiled, so I don’t know the name of the type to use. The elements in an array of anonymously typed objects must all define the same properties; otherwise, the compiler can’t work out what the array type should be.

To demonstrate this, I have changed the output from the example in listing 5.47 so that it shows the type name rather than the value of the Name property.

Listing 5.47 Displaying the type name in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            var products = new[] {
                new { Name = "Kayak", Price = 275M },
                new { Name = "Lifejacket", Price = 48.95M },
                new { Name = "Soccer ball", Price = 19.50M },
                new { Name = "Corner flag", Price = 34.95M }
            };

            return View(products.Select(p => p.GetType().Name));
        }
    }
}

All the objects in the array have been assigned the same type, which you can see if you run the example. The type name isn’t user-friendly but isn’t intended to be used directly, and you may see a different name than the one shown in the following output:

<>f__AnonymousType0`2
<>f__AnonymousType0`2
<>f__AnonymousType0`2
<>f__AnonymousType0`2

5.12 Using default implementations in interfaces

C# provides the ability to define default implementations for properties and methods defined by interfaces. This may seem like an odd feature because an interface is intended to be a description of features without specifying an implementation, but this addition to C# makes it possible to update interfaces without breaking the existing implementations of them.

Add a class file named IProductSelection.cs to the Models folder and use it to define the interface shown in listing 5.48.

Listing 5.48 The contents of the IProductSelection.cs file in the Models folder

namespace LanguageFeatures.Models {

    public interface IProductSelection {

        IEnumerable<Product>? Products { get; }
    }
}

Update the ShoppingCart class to implement the new interface, as shown in listing 5.49.

Listing 5.49 Implementing an interface in the ShoppingCart.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class ShoppingCart : IProductSelection {
        private List<Product> products = new();

        public ShoppingCart(params Product[] prods) {
            products.AddRange(prods);
        }

        public IEnumerable<Product>? Products { get => products; }
    }
}

Listing 5.50 updates the Home controller so that it uses the ShoppingCart class.

Listing 5.50 Using an interface in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            IProductSelection cart = new ShoppingCart(
                new Product { Name = "Kayak", Price = 275M },
                new Product { Name = "Lifejacket", Price = 48.95M },
                new Product { Name = "Soccer ball", Price = 19.50M },
                new Product { Name = "Corner flag", Price = 34.95M }
            );
            return View(cart.Products?.Select(p => p.Name));
        }
    }
}

This is the familiar use of an interface, and if you restart ASP.NET Core and request http://localhost:5000, you will see the following output in the browser:

Kayak
Lifejacket
Soccer ball
Corner flag

If I want to add a new feature to the interface, I must locate and update all the classes that implement it, which can be difficult, especially if an interface is used by other development teams in their projects. This is where the default implementation feature can be used, allowing new features to be added to an interface, as shown in listing 5.51.

Listing 5.51 Adding a feature in the IProductSelection.cs file in the Models folder

namespace LanguageFeatures.Models {

    public interface IProductSelection {

        IEnumerable<Product>? Products { get; }

        IEnumerable<string>? Names => Products?.Select(p => p.Name);
    }
}

The listing defines a Names property and provides a default implementation, which means that consumers of the IProductSelection interface can use the Names property even if it isn’t defined by implementation classes, as shown in listing 5.52.

Listing 5.52 Using a default implementation in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            IProductSelection cart = new ShoppingCart(
                new Product { Name = "Kayak", Price = 275M },
                new Product { Name = "Lifejacket", Price = 48.95M },
                new Product { Name = "Soccer ball", Price = 19.50M },
                new Product { Name = "Corner flag", Price = 34.95M }
            );
            return View(cart.Names);
        }
    }
}

The ShoppingCart class has not been modified, but the Index method can use the default implementation of the Names property. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser:

Kayak
Lifejacket
Soccer ball
Corner flag

5.13 Using asynchronous methods

Asynchronous methods perform work in the background and notify you when they are complete, allowing your code to take care of other business while the background work is performed. Asynchronous methods are an important tool in removing bottlenecks from code and allow applications to take advantage of multiple processors and processor cores to perform work in parallel.

In ASP.NET Core, asynchronous methods can be used to improve the overall performance of an application by allowing the server more flexibility in the way that requests are scheduled and executed. Two C# keywords—async and await—are used to perform work asynchronously.
5.13.1 Working with tasks directly

C# and .NET have excellent support for asynchronous methods, but the code has tended to be verbose, and developers who are not used to parallel programming often get bogged down by the unusual syntax. To create an example, add a class file called MyAsyncMethods.cs to the Models folder and add the code shown in listing 5.53.

Listing 5.53 The contents of the MyAsyncMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class MyAsyncMethods {

        public static Task<long?> GetPageLength() {
            HttpClient client = new HttpClient();
            var httpTask = client.GetAsync("http://manning.com");
            return httpTask.ContinueWith((Task<HttpResponseMessage> 
                    antecedent) => {
                return antecedent.Result.Content.Headers.ContentLength;
            });
        }
    }
}

This method uses a System.Net.Http.HttpClient object to request the contents of the Manning home page and returns its length. .NET represents work that will be done asynchronously as a Task. Task objects are strongly typed based on the result that the background work produces. So, when I call the HttpClient.GetAsync method, what I get back is a Task<HttpResponseMessage>. This tells me that the request will be performed in the background and that the result of the request will be an HttpResponseMessage object.

TIP When I use words like background, I am skipping over a lot of detail to make just the key points that are important to the world of ASP.NET Core. The .NET support for asynchronous methods and parallel programming is excellent, and I encourage you to learn more about it if you want to create truly high-performing applications that can take advantage of multicore and multiprocessor hardware. You will see how ASP.NET Core makes it easy to create asynchronous web applications throughout this book as I introduce different features.

The part that most programmers get bogged down with is the continuation, which is the mechanism by which you specify what you want to happen when the task is complete. In the example, I have used the ContinueWith method to process the HttpResponseMessage object I get from the HttpClient.GetAsync method, which I do with a lambda expression that returns the value of a property that contains the length of the content I get from the Manning web server. Here is the continuation code:

...
return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => {
    return antecedent.Result.Content.Headers.ContentLength;
});
...

Notice that I use the return keyword twice. This is the part that causes confusion. The first use of the return keyword specifies that I am returning a Task<HttpResponseMessage> object, which, when the task is complete, will return the length of the ContentLength header. The ContentLength header returns a long? result (a nullable long value), and this means the result of my GetPageLength method is Task<long?>, like this:

...
public static Task<long?> GetPageLength() {
...

Do not worry if this does not make sense—you are not alone in your confusion. It is for this reason that Microsoft added keywords to C# to simplify asynchronous methods.

5.13.2 Applying the async and await keywords

Microsoft introduced two keywords to C# that simplify using asynchronous methods like HttpClient.GetAsync. The keywords are async and await, and you can see how I have used them to simplify my example method in listing 5.54.

Listing 5.54 Using the async and await keywords in the MyAsyncMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class MyAsyncMethods {

        public async static Task<long?> GetPageLength() {
            HttpClient client = new HttpClient();
            var httpMessage = await client.GetAsync("http://manning.com");
            return httpMessage.Content.Headers.ContentLength;
        }
    }
}

I used the await keyword when calling the asynchronous method. This tells the C# compiler that I want to wait for the result of the Task that the GetAsync method returns and then carry on executing other statements in the same method.

Applying the await keyword means I can treat the result from the GetAsync method as though it were a regular method and just assign the HttpResponseMessage object that it returns to a variable. Even better, I can then use the return keyword in the normal way to produce a result from another method—in this case, the value of the ContentLength property. This is a much more natural technique, and it means I do not have to worry about the ContinueWith method and multiple uses of the return keyword.

When you use the await keyword, you must also add the async keyword to the method signature, as I have done in the example. The method result type does not change—my example GetPageLength method still returns a Task<long?>. This is because await and async are implemented using some clever compiler tricks, meaning that they allow a more natural syntax, but they do not change what is happening in the methods to which they are applied. Someone who is calling my GetPageLength method still has to deal with a Task<long?> result because there is still a background operation that produces a nullable long—although, of course, that programmer can also choose to use the await and async keywords.

This pattern follows through into the controller, which makes it easy to write asynchronous action methods, as shown in listing 5.55.

Note You can also use the async and await keywords in lambda expressions, which I demonstrate in later chapters.

Listing 5.55 An asynchronous action method in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public async Task<ViewResult> Index() {
            long? length = await MyAsyncMethods.GetPageLength();
            return View(new string[] { $"Length: {length}" });
        }
    }
}

I have changed the result of the Index action method to Task<ViewResult>, which declares that the action method will return a Task that will produce a ViewResult object when it completes, which will provide details of the view that should be rendered and the data that it requires. I have added the async keyword to the method’s definition, which allows me to use the await keyword when calling the MyAsyncMethods.GetPathLength method. .NET takes care of dealing with the continuations, and the result is asynchronous code that is easy to write, easy to read, and easy to maintain. Restart ASP.NET Core and request http://localhost:5000, and you will see output similar to the following (although with a different length since the content of the Manning website changes often):

Length: 472922

5.13.3 Using an asynchronous enumerable

An asynchronous enumerable describes a sequence of values that will be generated over time. To demonstrate the issue that this feature addresses, listing 5.56 adds a method to the MyAsyncMethods class.

Listing 5.56 Adding a method in the MyAsyncMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class MyAsyncMethods {

        public async static Task<long?> GetPageLength() {
            HttpClient client = new HttpClient();
            var httpMessage = await client.GetAsync("http://manning.com");
            return httpMessage.Content.Headers.ContentLength;
        }

        public static async Task<IEnumerable<long?>>
                GetPageLengths(List<string> output, 
                    params string[] urls) {
            List<long?> results = new List<long?>();
            HttpClient client = new HttpClient();
            foreach (string url in urls) {
                output.Add($"Started request for {url}");
                var httpMessage = await client.GetAsync($"http://{url}");
                results.Add(httpMessage.Content.Headers.ContentLength);
                output.Add($"Completed request for {url}");
            }
            return results;
        }
    }
}

The GetPageLengths method makes HTTP requests to a series of websites and gets their length. The requests are performed asynchronously, but there is no way to feed the results back to the method’s caller as they arrive. Instead, the method waits until all the requests are complete and then returns all the results in one go. In addition to the URLs that will be requested, this method accepts a List<string> to which I add messages in order to highlight how the code works. Listing 5.57 updates the Index action method of the Home controller to use the new method.

Listing 5.57 Using the new method in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public async Task<ViewResult> Index() {
            List<string> output = new List<string>();
            foreach (long? len in await MyAsyncMethods.GetPageLengths(
                    output,
                    "manning.com", "microsoft.com", "amazon.com")) {
                output.Add($"Page length: { len}");
            }
            return View(output);
        }
    }
}

The action method enumerates the sequence produced by the GetPageLengths method and adds each result to the List<string> object, which produces an ordered sequence of messages showing the interaction between the foreach loop in the Index method that processes the results and the foreach loop in the GetPageLengths method that generates them. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser (which may take several seconds to appear and may have different page lengths):

Started request for manning.com
Completed request for manning.com
Started request for microsoft.com
Completed request for microsoft.com
Started request for amazon.com
Completed request for amazon.com
Page length: 26973
Page length: 199526
Page length: 357777

You can see that the Index action method doesn’t receive the results until all the HTTP requests have been completed. This is the problem that the asynchronous enumerable feature solves, as shown in listing 5.58.

Listing 5.58 Using an asynchronous enumerable in the MyAsyncMethods.cs file in the Models folder

namespace LanguageFeatures.Models {

    public class MyAsyncMethods {

        public async static Task<long?> GetPageLength() {
            HttpClient client = new HttpClient();
            var httpMessage = await client.GetAsync("http://manning.com");
            return httpMessage.Content.Headers.ContentLength;
        }

        public static async IAsyncEnumerable<long?>
                GetPageLengths(List<string> output, 
                    params string[] urls) {
            HttpClient client = new HttpClient();
            foreach (string url in urls) {
                output.Add($"Started request for {url}");
                var httpMessage = await client.GetAsync($"http://{url}");
                output.Add($"Completed request for {url}");
                yield return httpMessage.Content.Headers.ContentLength;
            }
        }
    }
}

The methods result is IAsyncEnumerable<long?>, which denotes an asynchronous sequence of nullable long values. This result type has special support in .NET Core and works with standard yield return statements, which isn’t otherwise possible because the result constraints for asynchronous methods conflict with the yield keyword. Listing 5.59 updates the controller to use the revised method.

Listing 5.59 Using an asynchronous enumerable in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public async Task<ViewResult> Index() {
            List<string> output = new List<string>();
            await foreach (long? len in MyAsyncMethods.GetPageLengths(
                    output,
                    "manning.com", "microsoft.com", "amazon.com")) {
                output.Add($"Page length: { len}");
            }
            return View(output);
        }
    }
}

The difference is that the await keyword is applied before the foreach keyword and not before the call to the async method. Restart ASP.NET Core and request http://localhost:5000; once the HTTP requests are complete, you will see that the order of the response messages has changed, like this:

Started request for manning.com
Completed request for manning.com
Page length: 26973
Started request for microsoft.com
Completed request for microsoft.com
Page length: 199528
Started request for amazon.com
Completed request for amazon.com
Page length: 441398

The controller receives the next result in the sequence as it is produced. As I explain in chapter 19, ASP.NET Core has special support for using IAsyncEnumerable<T> results in web services, allowing data values to be serialized as the values in the sequence are generated.

5.14 Getting names

There are many tasks in web application development in which you need to refer to the name of an argument, variable, method, or class. Common examples include when you throw an exception or create a validation error when processing input from the user. The traditional approach has been to use a string value hard-coded with the name, as shown in listing 5.60.

Listing 5.60 Hard-coding a name in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            var products = new[] {
                new { Name = "Kayak", Price = 275M },
                new { Name = "Lifejacket", Price = 48.95M },
                new { Name = "Soccer ball", Price = 19.50M },
                new { Name = "Corner flag", Price = 34.95M }
            };
            return View(products.Select(p => 
                $"Name: {p.Name}, Price: {p.Price}"));
        }
    }
}

The call to the LINQ Select method generates a sequence of strings, each of which contains a hard-coded reference to the Name and Price properties. Restart ASP.NET Core and request http://localhost:5000, and you will see the following output in the browser window:

Name: Kayak, Price: 275
Name: Lifejacket, Price: 48.95
Name: Soccer ball, Price: 19.50
Name: Corner flag, Price: 34.95

This approach is prone to errors, either because the name was mistyped or because the code was refactored and the name in the string isn’t correctly updated. C# supports the nameof expression, in which the compiler takes responsibility for producing a name string, as shown in listing 5.61.

Listing 5.61 Using nameof expressions in the HomeController.cs file in the Controllers folder

namespace LanguageFeatures.Controllers {
    public class HomeController : Controller {

        public ViewResult Index() {
            var products = new[] {
                new { Name = "Kayak", Price = 275M },
                new { Name = "Lifejacket", Price = 48.95M },
                new { Name = "Soccer ball", Price = 19.50M },
                new { Name = "Corner flag", Price = 34.95M }
            };
            return View(products.Select(p =>
            $"{nameof(p.Name)}: {p.Name}, {nameof(p.Price)}: {p.Price}"));
        }
    }
}

The compiler processes a reference such as p.Name so that only the last part is included in the string, producing the same output as in previous examples. There is IntelliSense support for nameof expressions, so you will be prompted to select references, and expressions will be correctly updated when you refactor code. Since the compiler is responsible for dealing with nameof, using an invalid reference causes a compiler error, which prevents incorrect or outdated references from escaping notice.

Summary

• Top-level statements allow code to be defined outside of a class, which can make ASP.NET Core configuration more concise.

• Global using statements take effect throughout a project so that namespaces don’t have to be imported in individual C# files.

• Null state analysis ensures that null values are only assigned to nullable types and that values are read safely.

• String interpolation allows data values to be composed into strings.

• Object initialization patterns simplify the code required to create objects.

• Target-typed expressions omit the type name from the new statement.

• Pattern matching is used to execute code when a value has specific characteristics.

• Extension methods allow new functionality to be added to a type without needing to modify the class file.

• Lambda expressions are a concise way to express functions.

• Interfaces can be defined with default implementations, which means it is possible to modify the interface without breaking implementation classes.

• The async and await keywords are used to create asynchronous methods without needing to work directly with tasks and continuations.

Pro ASP.NET Core 7 Chapter 3 Your first ASP.NET Core application

3 Your first ASP.NET Core application

This chapter covers

• Using ASP.NET Core to create an application that accepts RSVP responses
• Creating a simple data model
• Creating a controller and view that presents and processes a form
• Validating user data and displaying validation errors
• Applying CSS styles to the HTML generated by the application

Now that you are set up for ASP.NET Core development, it is time to create a simple application. In this chapter, you’ll create a data-entry application using ASP.NET Core. My goal is to demonstrate ASP.NET Core in action, so I will pick up the pace a little and skip over some of the explanations as to how things work behind the scenes. But don’t worry; I’ll revisit these topics in-depth in later chapters.

3.1 Setting the scene

Imagine that a friend has decided to host a New Year’s Eve party and that she has asked me to create a web app that allows her invitees to electronically RSVP. She has asked for these four key features:

• A home page that shows information about the party
• A form that can be used to RSVP
• Validation for the RSVP form, which will display a thank-you page
• A summary page that shows who is coming to the party

In this chapter, I create an ASP.NET Core project and use it to create a simple application that contains these features; once everything works, I’ll apply some styling to improve the appearance of the finished application.

3.2 Creating the project

Open a PowerShell command prompt from the Windows Start menu, navigate to a convenient location, and run the commands in listing 3.1 to create a project named PartyInvites.

TIP You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/manningbooks/pro-asp.net-core-7. See chapter 1 for how to get help if you have problems running the examples.

Listing 3.1 Creating a new project

dotnet new globaljson --sdk-version 7.0.100 --output PartyInvites
dotnet new mvc --no-https --output PartyInvites --framework net7.0
dotnet new sln -o PartyInvites
dotnet sln PartyInvites add PartyInvites

These are the same commands I used to create the project in chapter 2. These commands ensure you get the right project starting point that uses the required version of .NET.

3.2.1 Preparing the project

Open the project (by opening the PartyInvites.sln file with Visual Studio or the PartyInvites folder in Visual Studio Code) and change the contents of the launchSettings.json file in the Properties folder, as shown in listing 3.2, to set the port that will be used to listen for HTTP requests.

Listing 3.2 Setting ports in the launchSettings.json file in the Properties folder

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 0
    }
  },
  "profiles": {
    "PartyInvites": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Replace the contents of the HomeController.cs file in the Controllers folder with the code shown in listing 3.3.

Listing 3.3 The new contents of the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
        
        public IActionResult Index() {
            return View();
        }
    }
}

This provides a clean starting point for the new application, defining a single action method that selects the default view for rendering. To provide a welcome message to party invitees, open the Index.cshtml file in the Views/Home folder and replace the contents with those shown in listing 3.4.

Listing 3.4 Replacing the contents of the Index.cshtml file in the Views/Home folder

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Party!</title>
</head>
<body>
    <div>
        <div>
            We're going to have an exciting party.<br />
            (To do: sell it better. Add pictures or something.)
        </div>
    </div>
</body>
</html>

Run the command shown in listing 3.5 in the PartyInvites folder to compile and execute the project.

Listing 3.5 Compiling and running the project

dotnet watch

Once the project has started, a new browser window will be opened, and you will see the details of the party (well, the placeholder for the details, but you get the idea), as shown in figure 3.1.

Figure 3.1 Adding to the view HTML

Leave the dotnet watch command running. As you make changes to the project, you will see that the code is automatically recompiled and that changes are automatically displayed in the browser.

If you make a mistake following the examples, you may find that the dotnet watch command indicates that it can’t automatically update the browser. If that happens, select the option to restart the application.

3.2.2 Adding a data model

The data model is the most important part of any ASP.NET Core application. The model is the representation of the real-world objects, processes, and rules that define the subject, known as the domain, of the application. The model, often referred to as a domain model, contains the C# objects (known as domain objects) that make up the universe of the application and the methods that manipulate them. In most projects, the job of the ASP.NET Core application is to provide the user with access to the data model and the features that allow the user to interact with it.

The convention for an ASP.NET Core application is that the data model classes are defined in a folder named Models, which was added to the project by the template used in listing 3.1.

I don’t need a complex model for the PartyInvites project because it is such a simple application. I need just one domain class that I will call GuestResponse. This object will represent an RSVP from an invitee.

If you are using Visual Studio, right-click the Models folder and select Add > Class from the pop-up menu. Set the name of the class to GuestResponse.cs and click the Add button. If you are using Visual Studio Code, right-click the Models folder, select New File, and enter GuestResponse.cs as the file name. Use the new file to define the class shown in listing 3.6.

Listing 3.6 The contents of the GuestResponse.cs file in the Models folder

namespace PartyInvites.Models {

    public class GuestResponse {
 
        public string? Name { get; set; }
        public string? Email { get; set; }
        public string? Phone { get; set; }
        public bool? WillAttend { get; set; }
    }
}

Notice that all the properties defined by the GuestResponse class are nullable. I explain why this is important in the “Adding Validation” section later in the chapter.

Restarting the automatic build

You may see a warning produced by the dotnet watch command telling you that a hot reload cannot be applied. The dotnet watch command can’t cope with every type of change, and some changes cause the automatic rebuild process to fail. You will see this prompt at the command line:

watch : Do you want to restart your app 
    - Yes (y) / No (n) / Always (a) / Never (v)?

Press a to always rebuild the project. Microsoft makes frequent improvements to the dotnet watch command and so the actions that trigger this problem change.

3.2.3 Creating a second action and view

One of my application goals is to include an RSVP form, which means I need to define an action method that can receive requests for that form. A single controller class can define multiple action methods, and the convention is to group related actions in the same controller. Listing 3.7 adds a new action method to the Home controller. Controllers can return different result types, which are explained in later chapters.

Listing 3.7 Adding an action in the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
        
        public IActionResult Index() {
            return View();
        }
 
        public ViewResult RsvpForm() {
            return View();
        }
    }
}

Both action methods invoke the View method without arguments, which may seem odd, but remember that the Razor view engine will use the name of the action method when looking for a view file, as explained in chapter 2. That means the result from the Index action method tells Razor to look for a view called Index.cshtml, while the result from the RsvpForm action method tells Razor to look for a view called RsvpForm.cshtml.

If you are using Visual Studio, right-click the Views/Home folder and select Add > New Item from the pop-up menu. Select the Razor View – Empty item, set the name to RsvpForm.cshtml, and click the Add button to create the file. Replace the contents with those shown in listing 3.8.

If you are using Visual Studio Code, right-click the Views/Home folder and select New File from the pop-up menu. Set the name of the file to RsvpForm.cshtml and add the contents shown in listing 3.8.

Listing 3.8 The contents of the RsvpForm.cshtml file in the Views/Home folder

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
</head>
<body>
    <div>
        This is the RsvpForm.cshtml View
    </div>
</body>
</html>

This content is just static HTML for the moment. Use the browser to request http://localhost:5000/home/rsvpform. The Razor view engine locates the RsvpForm.cshtml file and uses it to produce a response, as shown in figure 3.2.

Figure 3.2 Rendering a second view

3.2.4 Linking action methods

I want to be able to create a link from the Index view so that guests can see the RsvpForm view without having to know the URL that targets a specific action method, as shown in listing 3.9.

Listing 3.9 Adding a link in the Index.cshtml file in the Views/Home folder

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Party!</title>
</head>
<body>
    <div>
        <div>
            We're going to have an exciting party.<br />
            (To do: sell it better. Add pictures or something.)
        </div>
        <a asp-action="RsvpForm">RSVP Now</a>
    </div>
</body>
</html>

The addition to the listing is an a element that has an asp-action attribute. The attribute is an example of a tag helper attribute, which is an instruction for Razor that will be performed when the view is rendered. The asp-action attribute is an instruction to add an href attribute to the a element that contains a URL for an action method. I explain how tag helpers work in chapters 25–27, but this tag helper tells Razor to insert a URL for an action method defined by the same controller for which the current view is being rendered.

Use the browser to request http://localhost:5000, and you will see the link that the helper has created, as shown in figure 3.3.

Figure 3.3 Linking between action methods

Roll the mouse over the RSVP Now link in the browser. You will see that the link points to http://localhost:5000/Home/RsvpForm.

There is an important principle at work here, which is that you should use the features provided by ASP.NET Core to generate URLs, rather than hard-code them into your views. When the tag helper created the href attribute for the a element, it inspected the configuration of the application to figure out what the URL should be. This allows the configuration of the application to be changed to support different URL formats without needing to update any views.

3.2.5 Building the form

Now that I have created the view and can reach it from the Index view, I am going to build out the contents of the RsvpForm.cshtml file to turn it into an HTML form for editing GuestResponse objects, as shown in listing 3.10.

Listing 3.10 Creating a form view in the RsvpForm.cshtml file in the Views/Home folder

@model PartyInvites.Models.GuestResponse
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
</head>
<body>
    <form asp-action="RsvpForm" method="post">
        <div>
            <label asp-for="Name">Your name:</label>
            <input asp-for="Name" />
        </div>
        <div>
            <label asp-for="Email">Your email:</label>
            <input asp-for="Email" />
        </div>
        <div>
            <label asp-for="Phone">Your phone:</label>
            <input asp-for="Phone" />
        </div>
        <div>
            <label asp-for="WillAttend">Will you attend?</label>
            <select asp-for="WillAttend">
                <option value="">Choose an option</option>
                <option value="true">Yes, I'll be there</option>
                <option value="false">No, I can't come</option>
            </select>
        </div>
        <button type="submit">Submit RSVP</button>
    </form>
</body>
</html>

The @model expression specifies that the view expects to receive a GuestResponse object as its view model. I have defined a label and input element for each property of the GuestResponse model class (or, in the case of the WillAttend property, a select element). Each element is associated with the model property using the asp-for attribute, which is another tag helper attribute. The tag helper attributes configure the elements to tie them to the view model object. Here is an example of the HTML that the tag helpers produce:

<p>
    <label for="Name">Your name:</label>
    <input type="text" id="Name" name="Name" value="">
</p>

The asp-for attribute on the label element sets the value of the for attribute. The asp-for attribute on the input element sets the id and name elements. This may not look especially useful, but you will see that associating elements with a model property offers additional advantages as the application functionality is defined.

Of more immediate use is the asp-action attribute applied to the form element, which uses the application’s URL routing configuration to set the action attribute to a URL that will target a specific action method, like this:

<form method="post" action="/Home/RsvpForm">

As with the helper attribute I applied to the a element, the benefit of this approach is that when you change the system of URLs that the application uses, the content generated by the tag helpers will reflect the changes automatically.

Use the browser to request http://localhost:5000 and click the RSVP Now link to see the form, as shown in figure 3.4.

Figure 3.4 Adding an HTML form to the application

3.2.6 Receiving form data

I have not yet told ASP.NET Core what I want to do when the form is posted to the server. As things stand, clicking the Submit RSVP button just clears any values you have entered in the form. That is because the form posts back to the RsvpForm action method in the Home controller, which just renders the view again. To receive and process submitted form data, I am going to use an important feature of controllers. I will add a second RsvpForm action method to create the following:

• A method that responds to HTTP GET requests: A GET request is what a browser issues normally each time someone clicks a link. This version of the action will be responsible for displaying the initial blank form when someone first visits /Home/RsvpForm.

• A method that responds to HTTP POST requests: The form element defined in listing 3.10 sets the method attribute to post, which causes the form data to be sent to the server as a POST request. This version of the action will be responsible for receiving submitted data and deciding what to do with it.

Handling GET and POST requests in separate C# methods helps to keep my controller code tidy since the two methods have different responsibilities. Both action methods are invoked by the same URL, but ASP.NET Core makes sure that the appropriate method is called, based on whether I am dealing with a GET or POST request. Listing 3.11 shows the changes to the HomeController class.

Listing 3.11 Adding a method in the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
        
        public IActionResult Index() {
            return View();
        }
 
        [HttpGet]
        public ViewResult RsvpForm() {
            return View();
        }
 
        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse) {
            // TODO: store response from guest
            return View();
        }
    }
}

I have added the HttpGet attribute to the existing RsvpForm action method, which declares that this method should be used only for GET requests. I then added an overloaded version of the RsvpForm method, which accepts a GuestResponse object. I applied the HttpPost attribute to this method, which declares it will deal with POST requests. I explain how these additions to the listing work in the following sections. I also imported the PartyInvites.Models namespace—this is just so I can refer to the GuestResponse model type without needing to qualify the class name.

Understanding model binding

The first overload of the RsvpForm action method renders the same view as before—the RsvpForm.cshtml file—to generate the form shown in figure 3.4. The second overload is more interesting because of the parameter, but given that the action method will be invoked in response to an HTTP POST request and that the GuestResponse type is a C# class, how are the two connected?

The answer is model binding, a useful ASP.NET Core feature whereby incoming data is parsed and the key-value pairs in the HTTP request are used to populate properties of domain model types.

Model binding is a powerful and customizable feature that eliminates the grind of dealing with HTTP requests directly and lets you work with C# objects rather than dealing with individual data values sent by the browser. The GuestResponse object that is passed as the parameter to the action method is automatically populated with the data from the form fields. I dive into the details of model binding in chapter 28.

To demonstrate how model binding works, I need to do some preparatory work. One of the application goals is to present a summary page with details of who is attending the party, which means that I need to keep track of the responses that I receive. I am going to do this by creating an in-memory collection of objects. This isn’t useful in a real application because the response data will be lost when the application is stopped or restarted, but this approach will allow me to keep the focus on ASP.NET Core and create an application that can easily be reset to its initial state. Later chapters will demonstrate persistent data storage.

Add a class file named Repository.cs to the Models folder and use it to define the class shown in listing 3.12.

Listing 3.12 The contents of the Repository.cs file in the Models folder

namespace PartyInvites.Models {
    public static class Repository {
        private static List<GuestResponse> responses = new();
 
        public static IEnumerable<GuestResponse> Responses => responses;
 
        public static void AddResponse(GuestResponse response) {
            Console.WriteLine(response);
            responses.Add(response);
        }
    }
}

The Repository class and its members are static, which will make it easy for me to store and retrieve data from different places in the application. ASP.NET Core provides a more sophisticated approach for defining common functionality, called dependency injection, which I describe in chapter 14, but a static class is a good way to get started for a simple application like this one.

If you are using Visual Studio, saving the contents of the Repository.cs file will trigger a warning produced by the dotnet watch command telling you that a hot reload cannot be applied, which is the same warning described earlier in the chapter for Visual Studio Code users. You will see this prompt at the command line:

watch : Do you want to restart your app 
    - Yes (y) / No (n) / Always (a) / Never (v)?
Press a to always rebuild the project.

Storing responses

Now that I have somewhere to store the data, I can update the action method that receives the HTTP POST requests, as shown in listing 3.13.

Listing 3.13 Updating an action in the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
        
        public IActionResult Index() {
            return View();
        }
                
        [HttpGet]
        public ViewResult RsvpForm() {
            return View();
        }
                
        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse) {
            Repository.AddResponse(guestResponse);
            return View("Thanks", guestResponse);
        }
    }
}

Before the POST version of the RsvpForm method is invoked, the ASP.NET Core model binding feature extracts values from the HTML form and assigns them to the properties of the GuestResponse object. The result is used as the argument when the method is invoked to handle the HTTP request, and all I have to do to deal with the form data sent in a request is to work with the GuestResponse object that is passed to the action method—in this case, to pass it as an argument to the Repository.AddResponse method so t hat the response can be stored.

3.2.7 Adding the Thanks view

The call to the View method in the RsvpForm action method creates a ViewResult that selects a view called Thanks and uses the GuestResponse object created by the model binder as the view model. Add a Razor View named Thanks.cshtml to the Views/Home folder with the content shown in listing 3.14 to present a response to the user.

Listing 3.14 The contents of the Thanks.cshtml file in the Views/Home folder

@model PartyInvites.Models.GuestResponse
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Thanks</title>
</head>
<body>
    <div>
        <h1>Thank you, @Model?.Name!</h1>
        @if (Model?.WillAttend == true) {
            @:It's great that you're coming. 
            @:The drinks are already in the fridge!
        } else {
            @:Sorry to hear that you can't make it, 
            @:but thanks for letting us know.
        }
    </div>
    Click <a asp-action="ListResponses">here</a> to see who is coming.
</body>
</html>

The HTML produced by the Thanks.cshtml view depends on the values assigned to the GuestResponse view model provided by the RsvpForm action method. To access the value of a property in the domain object, I use an @Model. expression. So, for example, to get the value of the Name property, I use the @Model.Name expression. Don’t worry if the Razor syntax doesn’t make sense—I explain it in more detail in chapter 21.

Now that I have created the Thanks view, I have a basic working example of handling a form. Use the browser to request http://localhost:5000, click the RSVP Now link, add some data to the form, and click the Submit RSVP button. You will see the response shown in figure 3.5 (although it will differ if your name is not Joe or you said you could not attend).

Figure 3.5 The Thanks view

3.2.8 Displaying responses
At the end of the Thanks.cshtml view, I added an a element to create a link to display the list of people who are coming to the party. I used the asp-action tag helper attribute to create a URL that targets an action method called ListResponses, like this:

...
Click <a asp-action="ListResponses">here</a> to see who is coming.
...

If you hover the mouse over the link that is displayed by the browser, you will see that it targets the /Home/ListResponses URL. This doesn’t correspond to any of the action methods in the Home controller, and if you click the link, you will see a 404 Not Found error response.

To add an endpoint that will handle the URL, I need to add another action method to the Home controller, as shown in listing 3.15.

Listing 3.15 Adding an action in the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
 
        public IActionResult Index() {
            return View();
        }
 
        [HttpGet]
        public ViewResult RsvpForm() {
            return View();
        }
 
        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse) {
            Repository.AddResponse(guestResponse);
            return View("Thanks", guestResponse);
        }
 
        public ViewResult ListResponses() {
            return View(Repository.Responses
                .Where(r => r.WillAttend == true));
        }
    }
}

The new action method is called ListResponses, and it calls the View method, using the Repository.Responses property as the argument. This will cause Razor to render the default view, using the action method name as the name of the view file, and to use the data from the repository as the view model. The view model data is filtered using LINQ so that only positive responses are provided to the view.

Add a Razor View named ListResponses.cshtml to the Views/Home folder with the content shown in listing 3.16.

Listing 3.16 Displaying data in the ListResponses.cshtml file in the Views/Home folder

 
@model IEnumerable<PartyInvites.Models.GuestResponse>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Responses</title>
</head>
<body>
    <h2>Here is the list of people attending the party</h2>
    <table>
        <thead>
            <tr><th>Name</th><th>Email</th><th>Phone</th></tr>
        </thead>
        <tbody>
            @foreach (PartyInvites.Models.GuestResponse r in Model!) {
                <tr>
                    <td>@r.Name</td>
                    <td>@r.Email</td>
                    <td>@r.Phone</td>
                </tr>
            }
        </tbody>
    </table>
</body>
</html>

Razor view files have the .cshtml file extension to denote a mix of C# code and HTML elements. You can see this in listing 3.16 where I have used an @foreach expression to process each of the GuestResponse objects that the action method passes to the view using the View method. Unlike a normal C# foreach loop, the body of a Razor @foreach expression contains HTML elements that are added to the response that will be sent back to the browser. In this view, each GuestResponse object generates a tr element that contains td elements populated with the value of an object property.

Use the browser to request http://localhost:5000, click the RSVP Now link, and fill in the form. Submit the form and then click the link to see a summary of the data that has been entered since the application was first started, as shown in figure 3.6. The view does not present the data in an appealing way, but it is enough for the moment, and I will address the styling of the application later in this chapter.

Figure 3.6 Showing a list of party attendees

3.2.9 Adding validation

I can now add data validation to the application. Without validation, users could enter nonsense data or even submit an empty form. In an ASP.NET Core application, validation rules are defined by applying attributes to model classes, which means the same validation rules can be applied in any form that uses that class. ASP.NET Core relies on attributes from the System.ComponentModel.DataAnnotations namespace, which I have applied to the GuestResponse class in listing 3.17.

Listing 3.17 Applying validation in the GuestResponse.cs file in the Models folder

using System.ComponentModel.DataAnnotations;

namespace PartyInvites.Models {

    public class GuestResponse {
 
        [Required(ErrorMessage = "Please enter your name")]
        public string? Name { get; set; }
 
        [Required(ErrorMessage = "Please enter your email address")]
        [EmailAddress]
        public string? Email { get; set; }
 
        [Required(ErrorMessage = "Please enter your phone number")]
        public string? Phone { get; set; }
 
        [Required(ErrorMessage = "Please specify whether you'll attend")]
        public bool? WillAttend { get; set; }
    }
}

ASP.NET Core detects the attributes and uses them to validate data during the model-binding process.

As noted earlier, I used nullable types to define the GuestResponse properties. This is useful for denoting properties that may not be assigned values, but it has a special value for the WillAttend property because it allows the Required validation attribute to work. If I had used a regular non-nullable bool, the value I received through modelbinding could be only true or false, and I would not be able to tell whether the user had selected a value. A nullable bool has three possible values: true, false, and null. The value of the WillAttend property will be null if the user has not selected a value, and this causes the Required attribute to report a validation error. This is a nice example of how ASP.NET Core elegantly blends C# features with HTML and HTTP.

I check to see whether there has been a validation problem using the ModelState.IsValid property in the action method that receives the form data, as shown in listing 3.18.

Listing 3.18 Checking for errors in the HomeController.cs file in the Controllers folder

using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {
    public class HomeController : Controller {
 
        public IActionResult Index() {
            return View();
        }
 
        [HttpGet]
        public ViewResult RsvpForm() {
            return View();
        }
 
        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse) {
            if (ModelState.IsValid) {
                Repository.AddResponse(guestResponse);
                return View("Thanks", guestResponse);
            } else {
                return View();
            }
        }
 
        public ViewResult ListResponses() {
            return View(Repository.Responses
                .Where(r => r.WillAttend == true));
        }
    }
}

The Controller base class provides a property called ModelState that provides details of the outcome of the model binding process. If the ModelState.IsValid property returns true, then I know that the model binder has been able to satisfy the validation constraints I specified through the attributes on the GuestResponse class. When this happens, I render the Thanks view, just as I did previously.

If the ModelState.IsValid property returns false, then I know that there are validation errors. The object returned by the ModelState property provides details of each problem that has been encountered, but I don’t need to get into that level of detail because I can rely on a useful feature that automates the process of asking the user to address any problems by calling the View method without any parameters.

When it renders a view, Razor has access to the details of any validation errors associated with the request, and tag helpers can access the details to display validation errors to the user. Listing 3.19 shows the addition of validation tag helper attributes to the RsvpForm view.

Listing 3.19 Adding a summary to the RsvpForm.cshtml file in the Views/Home folder

@model PartyInvites.Models.GuestResponse
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
</head>
<body>
    <form asp-action="RsvpForm" method="post">
        <div asp-validation-summary="All"></div>
        <div>
            <label asp-for="Name">Your name:</label>
            <input asp-for="Name" />
        </div>
        <div>
            <label asp-for="Email">Your email:</label>
            <input asp-for="Email" />
        </div>
        <div>
            <label asp-for="Phone">Your phone:</label>
            <input asp-for="Phone" />
        </div>
        <div>
            <label asp-for="WillAttend">Will you attend?</label>
            <select asp-for="WillAttend">
                <option value="">Choose an option</option>
                <option value="true">Yes, I'll be there</option>
                <option value="false">No, I can't come</option>
            </select>
        </div>
        <button type="submit">Submit RSVP</button>
    </form>
</body>
</html>

The asp-validation-summary attribute is applied to a div element, and it displays a list of validation errors when the view is rendered. The value for the asp-validation-summary attribute is a value from an enumeration called ValidationSummary, which specifies what types of validation errors the summary will contain. I specified All, which is a good starting point for most applications, and I describe the other values and explain how they work in chapter 29.

To see how the validation summary works, run the application, fill out the Name field, and submit the form without entering any other data. You will see a summary of validation errors, as shown in figure 3.7.

Figure 3.7 Displaying validation errors

The RsvpForm action method will not render the Thanks view until all the validation constraints applied to the GuestResponse class have been satisfied. Notice that the data entered in the Name field was preserved and displayed again when Razor rendered the view with the validation summary. This is another benefit of model binding, and it simplifies working with form data.

Highlighting invalid fields

The tag helper attributes that associate model properties with elements have a handy feature that can be used in conjunction with model binding. When a model class property has failed validation, the helper attributes will generate slightly different HTML. Here is the input element that is generated for the Phone field when there is no validation error:

<input type="text" data-val="true"
     data-val-required="Please enter your phone number" id="Phone"
     name="Phone" value="">

For comparison, here is the same HTML element after the user has submitted the form without entering data into the text field (which is a validation error because I applied the Required attribute to the Phone property of the GuestResponse class):

<input type="text" class="input-validation-error"
     data-val="true" data-val-required="Please enter your phone number" id="Phone"
     name="Phone" value="">

I have highlighted the difference: the asp-for tag helper attribute added the input element to a class called input-validation-error. I can take advantage of this feature by creating a stylesheet that contains CSS styles for this class and the others that different HTML helper attributes use.

The convention in ASP.NET Core projects is that static content is placed into the wwwroot folder and organized by content type so that CSS stylesheets go into the wwwroot/css folder, JavaScript files go into the wwwroot/js folder, and so on.

TIP The project template used in listing 3.1 creates a site.css file in the wwwroot/css folder. You can ignore this file, which I don’t use in this chapter.

If you are using Visual Studio, right-click the wwwroot/css folder and select Add > New Item from the pop-up menu. Locate the Style Sheet item template, as shown in figure 3.8; set the name of the file to styles.css; and click the Add button.

Figure 3.8 Creating a CSS stylesheet

If you are using Visual Studio Code, right-click the wwwroot/css folder, select New File from the pop-up menu, and use styles.css as the file name. Regardless of which editor you use, replace the contents of the file with the styles shown in listing 3.20.

Listing 3.20 The contents of the styles.css file in the wwwroot/css folder

.field-validation-error {
    color: #f00;
}

.field-validation-valid {
    display: none;
}

.input-validation-error {
    border: 1px solid #f00;
    background-color: #fee;
}

.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}

.validation-summary-valid {
    display: none;
}

To apply this stylesheet, I added a link element to the head section of the RsvpForm view, as shown in listing 3.21.

Listing 3.21 Applying a stylesheet in the RsvpForm.cshtml file in the Views/Home folder

 
...
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
    <link rel="stylesheet" href="/css/styles.css" />
</head>
...

The link element uses the href attribute to specify the location of the stylesheet. Notice that the wwwroot folder is omitted from the URL. The default configuration for ASP.NET includes support for serving static content, such as images, CSS stylesheets, and JavaScript files, and it maps requests to the wwwroot folder automatically. With the application of the stylesheet, a more obvious validation error will be displayed when data is submitted that causes a validation error, as shown in figure 3.9.

Figure 3.9 Automatically highlighted validation errors

3.2.10 Styling the content
All the functional goals for the application are complete, but the overall appearance of the application is poor. When you create a project using the mvc template, as I did for the example in this chapter, some common client-side development packages are installed. While I am not a fan of using template projects, I do like the client-side libraries that Microsoft has chosen. One of them is called Bootstrap, which is a good CSS framework originally developed by Twitter that has become a major open-source project and a mainstay of web application development.

Styling the welcome view

The basic Bootstrap features work by applying classes to elements that correspond to CSS selectors defined in the files added to the wwwroot/lib/bootstrap folder. You can get full details of the classes that Bootstrap defines from http://getbootstrap.com, but you can see how I have applied some basic styling to the Index.cshtml view file in listing 3.22.

Listing 3.22 Adding Bootstrap to the Index.cshtml file in the Views/Home folder

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
    <title>Index</title>
</head>
<body>
    <div class="text-center m-2">
        <h3> We're going to have an exciting party!</h3>
        <h4>And YOU are invited!</h4>
        <a class="btn btn-primary" asp-action="RsvpForm">RSVP Now</a>
    </div>
</body>
</html>

I have added a link element whose href attribute loads the bootstrap.css file from the wwwroot/lib/bootstrap/dist/css folder. The convention is that third-party CSS and JavaScript packages are installed into the wwwroot/lib folder, and I describe the tool that is used to manage these packages in chapter 4.

Having imported the Bootstrap stylesheets, I need to style my elements. This is a simple example, so I need to use only a small number of Bootstrap CSS classes: text-center, btn, and btn-primary.

The text-center class centers the contents of an element and its children. The btn class styles a button, input, or a element as a pretty button, and the btn-primary class specifies which of a range of colors I want the button to be. You can see the effect by running the application, as shown in figure 3.10.

Figure 3.10 Styling a view

It will be obvious to you that I am not a web designer. In fact, as a child, I was excused from art lessons on the basis that I had absolutely no talent whatsoever. This had the happy result of making more time for math lessons but meant that my artistic skills have not developed beyond those of the average 10-year-old. For a real project, I would seek a professional to help design and style the content, but for this example, I am going it alone, and that means applying Bootstrap with as much restraint and consistency as I can muster.

Styling the form view

Bootstrap defines classes that can be used to style forms. I am not going to go into detail, but you can see how I have applied these classes in listing 3.23.

Listing 3.23 Adding styles to the RsvpForm.cshtml file in the Views/Home folder

@model PartyInvites.Models.GuestResponse
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
    <h5 class="bg-primary text-white text-center m-2 p-2">RSVP</h5>
    <form asp-action="RsvpForm" method="post" class="m-2">
        <div asp-validation-summary="All"></div>
        <div class="form-group">
            <label asp-for="Name" class="form-label">Your name:</label>
            <input asp-for="Name" class="form-control" />
        </div>
        <div class="form-group">
            <label asp-for="Email" class="form-label">Your email:</label>
            <input asp-for="Email"  class="form-control" />
        </div>
        <div class="form-group">
            <label asp-for="Phone" class="form-label">Your phone:</label>
            <input asp-for="Phone" class="form-control" />
        </div>
        <div class="form-group">
            <label asp-for="WillAttend" class="form-label">
                Will you attend?
            </label>
            <select asp-for="WillAttend" class="form-select">
                <option value="">Choose an option</option>
                <option value="true">Yes, I'll be there</option>
                <option value="false">No, I can't come</option>
            </select>
        </div>
        <button type="submit" class="btn btn-primary mt-3">
            Submit RSVP
        </button>
    </form>
</body>
</html>

The Bootstrap classes in this example create a header, just to give structure to the layout. To style the form, I have used the form-group class, which is used to style the element that contains the label and the associated input or select element, which is assigned to the form-control class. You can see the effect of the styles in figure 3.11.

Figure 3.11 Styling the RsvpForm view

Styling the thanks view

The next view file to style is Thanks.cshtml, and you can see how I have done this in listing 3.24, using CSS classes that are similar to the ones I used for the other views. To make an application easier to manage, it is a good principle to avoid duplicating code and markup wherever possible. ASP.NET Core provides several features to help reduce duplication, which I describe in later chapters. These features include Razor layouts (Chapter 22), partial views (Chapter 22), and view components (Chapter 24).

Listing 3.24 Applying styles to the Thanks.cshtml file in the Views/Home folder

@model PartyInvites.Models.GuestResponse
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Thanks</title>
        <link rel="stylesheet" 
            href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body class="text-center">
    <div>
        <h1>Thank you, @Model?.Name!</h1>
        @if (Model?.WillAttend == true) {
            @:It's great that you're coming. 
            @:The drinks are already in the fridge!
        } else {
            @:Sorry to hear that you can't make it, 
            @:but thanks for letting us know.
        }
    </div>
    Click <a asp-action="ListResponses">here</a> to see who is coming.
</body>
</html>

Figure 3.12 shows the effect of the styles.

Figure 3.12 Styling the Thanks view

Styling the list view

The final view to style is ListResponses, which presents the list of attendees. Styling the content follows the same approach as used for the other views, as shown in listing 3.25.

Listing 3.25 Adding styles to the ListResponses.cshtml file in the Views/Home folder

@model IEnumerable<PartyInvites.Models.GuestResponse>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Responses</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <div class="text-center p-2">
        <h2 class="text-center">
            Here is the list of people attending the party
        </h2>
        <table class="table table-bordered table-striped table-sm">
            <thead>
                <tr><th>Name</th><th>Email</th><th>Phone</th></tr>
            </thead>
            <tbody>
                @foreach (PartyInvites.Models.GuestResponse r in Model!) {
                    <tr>
                        <td>@r.Name</td>
                        <td>@r.Email</td>
                        <td>@r.Phone</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</body>
</html>

Figure 3.13 shows the way that the table of attendees is presented. Adding these styles to the view completes the example application, which now meets all the development goals and has an improved appearance.

Figure 3.13 Styling the ListResponses view

Summary

• ASP.NET Core projects are created with the dotnet new command.

• Controllers define action methods that are used to handle HTTP requests.

• Views generate HTML content that is used to respond to HTTP requests.

• Views can contain HTML elements that are bound to data model properties.

• Model binding is the process by which request data is parsed and assigned to the properties of objects that are passed to action methods for processing.

• The data in the request can be subjected to validation and errors can be displayed to the user within the same HTML form that was used to submit the data.

• The HTML content generated by views can be styled using the same CSS features that are applied to static HTML content.

Pro ASP.NET Core 7 Tenth Edition

阿里云与腾讯云的测试IP

阿里云测试IP

华东 1 oss-cn-hangzhou.aliyuncs.com
华东 2 oss-cn-shanghai.aliyuncs.com
华北 1 oss-cn-qingdao.aliyuncs.com
华北 2 oss-cn-beijing.aliyuncs.com
华北 3 oss-cn-zhangjiakou.aliyuncs.com
华北 5 oss-cn-huhehaote.aliyuncs.com
华南 1 oss-cn-shenzhen.aliyuncs.com
香港 oss-cn-hongkong.aliyuncs.com
美国西部 1 (硅谷) oss-us-west-1.aliyuncs.com
美国东部 1 (弗吉尼亚) oss-us-east-1.aliyuncs.com
亚太东南 1 (新加坡) oss-ap-southeast-1.aliyuncs.com
亚太东南 2 (悉尼) oss-ap-southeast-2.aliyuncs.com
亚太东南 3 (吉隆坡) oss-ap-southeast-3.aliyuncs.com
亚太东南 5 (雅加达) oss-ap-southeast-5.aliyuncs.com
亚太南部 1 (孟买) oss-ap-south-1.aliyuncs.com
亚太东北 1 (日本) oss-ap-northeast-1.aliyuncs.com
欧洲中部 1 (法兰克福) oss-eu-central-1.aliyuncs.com
中东东部 1 (迪拜) oss-me-east-1.aliyuncs.com

腾讯云测试IP

北京(华北)  ap-beijing  cos.ap-beijing.myqcloud.com
上海(华东)  ap-shanghai  cos.ap-shanghai.myqcloud.com
广州(华南)  ap-guangzhou  cos.ap-guangzhou.myqcloud.com
成都(西南)  ap-chengdu  cos.ap-chengdu.myqcloud.com
重庆(西南)  ap-chongqing  cos.ap-chongqing.myqcloud.com
香港  ap-hongkong  cos.ap-hongkong.myqcloud.com
新加坡  ap-singapore  cos.ap-singapore.myqcloud.com
多伦多  na-toronto  cos.na-toronto.myqcloud.com
法兰克福  eu-frankfurt  cos.eu-frankfurt.myqcloud.com
孟买  ap-mumbai  cos.ap-mumbai.myqcloud.com
首尔  ap-seoul  cos.ap-seoul.myqcloud.com
硅谷  na-siliconvalley  cos.na-siliconvalley.myqcloud.com
弗吉尼亚  na-ashburn  cos.na-ashburn.myqcloud.com
曼谷  ap-bangkok  cos.ap-bangkok.myqcloud.com
莫斯科  eu-moscow  cos.eu-moscow.myqcloud.com

Pro C#10 CHAPTER 12 Delegates, Events, and Lambda Expressions

CHAPTER 12

Delegates, Events, and Lambda Expressions

Up to this point in the text, most of the applications you have developed added various bits of code to Program.cs as top-level statements, which, in some way or another, sent requests to a given object. However, many applications require that an object be able to communicate back to the entity that created it using a callback mechanism. While callback mechanisms can be used in any application, they are especially critical for graphical user interfaces in that controls (such as a button) need to invoke external methods under the correct circumstances (when the button is clicked, when the mouse enters the button surface, etc.).
Under the .NET platform, the delegate type is the preferred means of defining and responding to callbacks within applications. Essentially, the .NET delegate type is a type-safe object that “points to” a method or a list of methods that can be invoked later. Unlike a traditional C++ function pointer, however, delegates are classes that have built-in support for multicasting.

■ Note In prior versions of .NET, delegates exposed asynchronous method invocation with
BeginInvoke()/EndInvoke(). While these are still generated by the compiler, they are not supported under
.NET. This is because the IAsyncResult()/BeginInvoke() pattern used by delegates has been replaced by the task-based async pattern. For more on asynchronous execution, see Chapter 15.

In this chapter, you will learn how to create and manipulate delegate types, and then you’ll investigate the C# event keyword, which streamlines the process of working with delegate types. Along the way, you will also examine several delegate-centric and event-centric language features of C#, including anonymous methods and method group conversions.
I wrap up this chapter by examining lambda expressions. Using the C# lambda operator (=>), you can specify a block of code statements (and the parameters to pass to those code statements) wherever a strongly typed delegate is required. As you will see, a lambda expression is little more than an anonymous method in disguise and provides a simplified approach to working with delegates. In addition, this same operation (as of .NET Framework 4.6 and later) can be used to implement a single-statement method or property using a concise syntax.

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_12

467

Understanding the Delegate Type
Before formally defining delegates, let’s gain a bit of perspective. Historically, the Windows API made frequent use of C-style function pointers to create entities termed callback functions, or simply callbacks. Using callbacks, programmers were able to configure one function to report back to (call back) another function in the application. With this approach, Windows developers were able to handle button clicking, mouse moving, menu selecting, and general bidirectional communications between two entities in memory.
In the .NET and .NET Core (and .NET 5+) Frameworks, callbacks are accomplished in a type-safe and object-oriented manner using delegates. A delegate is a type-safe object that points to another method (or possibly a list of methods) in the application, which can be invoked later. Specifically, a delegate maintains three important pieces of information.
•The address of the method on which it makes calls
•The parameters (if any) of this method
•The return type (if any) of this method

■ Note .NET delegates can point to either static or instance methods.

After a delegate object has been created and given the necessary information, it may dynamically invoke the method(s) it points to at runtime.

Defining a Delegate Type in C#
When you want to create a delegate type in C#, you use the delegate keyword. The name of your delegate type can be whatever you desire. However, you must define the delegate to match the signature of the method(s) it will point to. For example, the following delegate type (named BinaryOp) can point to any method that returns an integer and takes two integers as input parameters (you will build and use this delegate yourself a bit later in this chapter, so hang tight for now):

// This delegate can point to any method,
// taking two integers and returning an integer. public delegate int BinaryOp(int x, int y);

When the C# compiler processes delegate types, it automatically generates a sealed class deriving from System.MulticastDelegate. This class (in conjunction with its base class, System.Delegate) provides the necessary infrastructure for the delegate to hold onto a list of methods to be invoked later. For example, if you were to examine the BinaryOp delegate using ildasm.exe, you would find some of the details as shown here (you will build this full example in just a moment if you want to check for yourself):

//
// TypDefName:BinaryOp
// Extends : System.MulticastDelegate
// Method #1
//
// MethodName: .ctor
// ReturnType: Void
// 2 Arguments
// Argument #1: Object

// Argument #2: I
// 2 Parameters
// (1) ParamToken :Name : object flags: [none]
// (2) ParamToken : Name : method flags: [none]
// Method #2
//
// MethodName: Invoke
// ReturnType: I4
// 2 Arguments
// Argument #1: I4
// Argument #2: I4
// 2 Parameters
// (1) ParamToken : Name : x flags: [none]
// (2) ParamToken : Name : y flags: [none] //
// Method #3
//
// MethodName: BeginInvoke
// ReturnType: Class System.IAsyncResult
// 4 Arguments
// Argument #1: I4
// Argument #2: I4
// Argument #3: Class System.AsyncCallback
// Argument #4: Object
// 4 Parameters
// (1) ParamToken : Name : x flags: [none]
// (2) ParamToken : Name : y flags: [none]
// (3) ParamToken : Name : callback flags: [none]
// (4) ParamToken : Name : object flags: [none]
//
// Method #4
//
// MethodName: EndInvoke
// ReturnType: I4 (int32)
// 1 Arguments
// Argument #1: Class System.IAsyncResult
// 1 Parameters
// (1) ParamToken : Name : result flags: [none]

As you can see, the compiler-generated BinaryOp class defines three public methods. Invoke() is the key method in .NET, as it is used to invoke each method maintained by the delegate object in a synchronous manner, meaning the caller must wait for the call to complete before continuing its way. Strangely enough, the synchronous Invoke() method may not need to be called explicitly from your C# code. As you will see in just a bit, Invoke() is called behind the scenes when you use the appropriate C# syntax.

■ Note While BeginInvoke() and EndInvoke() are generated, they are not supported when running your code under .NET Core or .NET 5+. This can be frustrating, since you will not receive a compiler error but a runtime error if you use them.

Now, how exactly does the compiler know how to define the Invoke() method? To understand the process, here is the crux of the compiler-generated BinaryOp class type (bold italic marks the items specified by the defined delegate type):

sealed class BinaryOp : System.MulticastDelegate
{
public int Invoke(int x, int y);
...
}

First, notice that the parameters and return type defined for the Invoke() method exactly match the definition of the BinaryOp delegate.
Let’s see another example. Assume you have defined a delegate type that can point to any method returning a string and receiving three System.Boolean input parameters.

public delegate string MyDelegate (bool a, bool b, bool c);

This time, the compiler-generated class breaks down as follows:

sealed class MyDelegate : System.MulticastDelegate
{
public string Invoke(bool a, bool b, bool c);
...
}

Delegates can also “point to” methods that contain any number of out or ref parameters (as well as array parameters marked with the params keyword). For example, assume the following delegate type:

public delegate string MyOtherDelegate( out bool a, ref bool b, int c);

The signature of the Invoke() method looks as you would expect.
To summarize, a C# delegate type definition results in a sealed class with a compiler-generated method whose parameter and return types are based on the delegate’s declaration. The following pseudocode approximates the basic pattern:

// This is only pseudo-code!
public sealed class DelegateName : System.MulticastDelegate
{
public delegateReturnValue Invoke(allDelegateInputRefAndOutParams);
}

The System.MulticastDelegate and System.Delegate Base Classes
So, when you build a type using the C# delegate keyword, you are indirectly declaring a class type that derives from System.MulticastDelegate. This class provides descendants with access to a list that contains the addresses of the methods maintained by the delegate object, as well as several additional methods (and a few overloaded operators) to interact with the invocation list. Here are some select members of System. MulticastDelegate:

public abstract class MulticastDelegate : Delegate
{
// Returns the list of methods "pointed to."
public sealed override Delegate[] GetInvocationList();

// Overloaded operators.
public static bool operator == (MulticastDelegate d1, MulticastDelegate d2);
public static bool operator != (MulticastDelegate d1, MulticastDelegate d2);

// Used internally to manage the list of methods maintained by the delegate.
private IntPtr _invocationCount; private object _invocationList;
}

System.MulticastDelegate obtains additional functionality from its parent class, System.Delegate.
Here is a partial snapshot of the class definition:

public abstract class Delegate : ICloneable, ISerializable
{
// Methods to interact with the list of functions.
public static Delegate Combine(params Delegate[] delegates); public static Delegate Combine(Delegate a, Delegate b); public static Delegate Remove(
Delegate source, Delegate value); public static Delegate RemoveAll(
Delegate source, Delegate value);

// Overloaded operators.
public static bool operator ==(Delegate d1, Delegate d2); public static bool operator !=(Delegate d1, Delegate d2);

// Properties that expose the delegate target.
public MethodInfo Method { get; } public object Target { get; }
}

Now, understand that you can never directly derive from these base classes in your code (it is a compiler error to do so). Nevertheless, when you use the delegate keyword, you have indirectly created a class that “is-a” MulticastDelegate. Table 12-1 documents the core members common to all delegate types.

Table 12-1. Select Members of System.MulticastDelegate/System.Delegate

Member Meaning in Life
Method This property returns a System.Reflection.MethodInfo object that represents details of a static method maintained by the delegate.
Target If the method to be called is defined at the object level (rather than a static method), Target returns an object that represents the method maintained by the delegate. If the value returned from Target equals null, the method to be called is a static member.
Combine() This static method adds a method to the list maintained by the delegate. In C#, you trigger this method using the overloaded += operator as a shorthand notation.
GetInvocationList() This method returns an array of System.Delegate objects, each representing a method that may be invoked.
Remove() / RemoveAll() These static methods remove a method (or all methods) from the delegate’s invocation list. In C#, the Remove() method can be called indirectly using the overloaded -= operator.

The Simplest Possible Delegate Example
To be sure, delegates can cause some confusion when encountered for the first time. Thus, to get the ball rolling, let’s look at a simple Console Application program (named SimpleDelegate) that uses the BinaryOp delegate type you’ve seen previously. Here is the complete code, with analysis to follow:

//SimpleMath.cs
namespace SimpleDelegate;
// This class contains methods BinaryOp will
// point to.
public class SimpleMath
{
public static int Add(int x, int y) => x + y; public static int Subtract(int x, int y) => x - y;
}

//Program.cs
using SimpleDelegate;

Console.WriteLine(" Simple Delegate Example \n");

// Create a BinaryOp delegate object that
// "points to" SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);

// Invoke Add() method indirectly using delegate object. Console.WriteLine("10 + 10 is {0}", b(10, 10)); Console.ReadLine();

//Additional type definitions must be placed at the end of the
// top-level statements
// This delegate can point to any method,
// taking two integers and returning an integer. public delegate int BinaryOp(int x, int y);

■ Note recall from Chapter 3 that additional type declarations (in this example the BinaryOp delegate) must come after all top-level statements.

Again, notice the format of the BinaryOp delegate type declaration; it specifies that BinaryOp delegate objects can point to any method taking two integers and returning an integer (the actual name of the method pointed to is irrelevant). Here, you have created a class named SimpleMath, which defines two static methods that match the pattern defined by the BinaryOp delegate.
When you want to assign the target method to a given delegate object, simply pass in the name of the method to the delegate’s constructor.

// Create a BinaryOp delegate object that
// "points to" SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);

At this point, you can invoke the member pointed to using a syntax that looks like a direct function invocation.

// Invoke() is really called here! Console.WriteLine("10 + 10 is {0}", b(10, 10));

Under the hood, the runtime calls the compiler-generated Invoke() method on your MulticastDelegate-derived class. You can verify this for yourself if you open your assembly in ildasm.exe and examine the CIL code within the Main() method.

.method private hidebysig static void '

$'(string[] args) cil managed
{
...
callvirt instance int32 BinaryOp::Invoke(int32, int32)
}

C# does not require you to explicitly call Invoke() within your code base. Because BinaryOp can point to methods that take two arguments, the following code statement is also permissible:

Console.WriteLine("10 + 10 is {0}", b.Invoke(10, 10));

Recall that .NET delegates are type-safe. Therefore, if you attempt to create a delegate object pointing to a method that does not match the pattern, you receive a compile-time error. To illustrate, assume the SimpleMath class now defines an additional method named SquareNumber(), which takes a single integer as input.

public class SimpleMath
{
public static int SquareNumber(int a) => a * a;
}

Given that the BinaryOp delegate can point only to methods that take two integers and return an integer, the following code is illegal and will not compile:

// Compiler error! Method does not match delegate pattern! BinaryOp b2 = new BinaryOp(SimpleMath.SquareNumber);

Investigating a Delegate Object
Let’s spice up the current example by creating a static method (named DisplayDelegateInfo()) within the Program.cs file. This method will print out the names of the methods maintained by a delegate object, as well as the name of the class defining the method. To do this, you will iterate over the System.Delegate array returned by GetInvocationList(), invoking each object’s Target and Method properties.

static void DisplayDelegateInfo(Delegate delObj)
{
// Print the names of each member in the
// delegate's invocation list.
foreach (Delegate d in delObj.GetInvocationList())
{
Console.WriteLine("Method Name: {0}", d.Method); Console.WriteLine("Type Name: {0}", d.Target);
}
}

Assuming you have updated your Main() method to call this new helper method, as shown here:

BinaryOp b = new BinaryOp(SimpleMath.Add); DisplayDelegateInfo(b);

you would find the output shown next:

Simple Delegate Example Method Name: Int32 Add(Int32, Int32) Type Name:
10 + 10 is 20

Notice that the name of the target class (SimpleMath) is currently not displayed when calling the Target property. The reason has to do with the fact that your BinaryOp delegate is pointing to a static method and, therefore, there is no object to reference! However, if you update the Add() and Subtract() methods to be non-static (simply by deleting the static keywords), you could create an instance of the SimpleMath class and specify the methods to invoke using the object reference.

using SimpleDelegate;

Console.WriteLine(" Simple Delegate Example \n");

// Delegates can also point to instance methods as well. SimpleMath m = new SimpleMath();
BinaryOp b = new BinaryOp(m.Add);

// Show information about this object. DisplayDelegateInfo(b);

Console.WriteLine("10 + 10 is {0}", b(10, 10)); Console.ReadLine();

In this case, you would find the output shown here:

Simple Delegate Example Method Name: Int32 Add(Int32, Int32) Type Name: SimpleDelegate.SimpleMath 10 + 10 is 20

Sending Object State Notifications Using Delegates
Clearly, the previous SimpleDelegate example was intended to be purely illustrative in nature, given that there would be no compelling reason to define a delegate simply to add two numbers. To provide a more realistic use of delegate types, let’s use delegates to define a Car class that can inform external entities about its current engine state. To do so, you will take the following steps:
1.Define a new delegate type that will be used to send notifications to the caller.
2.Declare a member variable of this delegate in the Car class.
3.Create a helper function on the Car that allows the caller to specify the method to call back on.
4.Implement the Accelerate() method to invoke the delegate’s invocation list under the correct circumstances.
To begin, create a new Console Application project named CarDelegate. Now, define a new Car class that looks initially like this:

namespace CarDelegate; public class Car
{
// Internal state data.
public int CurrentSpeed { get; set; } public int MaxSpeed { get; set; } = 100; public string PetName { get; set; }

// Is the car alive or dead? private bool _carIsDead;

// Class constructors. public Car() {}
public Car(string name, int maxSp, int currSp)
{
CurrentSpeed = currSp;
MaxSpeed = maxSp;

PetName = name;
}
}

Now, consider the following updates, which address the first three points:

public class Car
{
...
// 1) Define a delegate type.
public delegate void CarEngineHandler(string msgForCaller);

// 2) Define a member variable of this delegate.
private CarEngineHandler _listOfHandlers;

// 3) Add registration function for the caller.
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
_listOfHandlers = methodToCall;
}
}

Notice in this example that you define the delegate types directly within the scope of the Car class, which is certainly not necessary but does help enforce the idea that the delegate works naturally with this class. The delegate type, CarEngineHandler, can point to any method taking a single string as input and void as a return value.
Next, note that you declare a private member variable of your delegate type (named _listOfHandlers) and a helper function (named RegisterWithCarEngine()) that allows the caller to assign a method to the delegate’s invocation list.

■ Note strictly speaking, you could have defined your delegate member variable as public, therefore avoiding the need to create additional registration methods. however, by defining the delegate member variable as private, you are enforcing encapsulation services and providing a more type-safe solution. You’ll revisit the risk of public delegate member variables later in this chapter when you look at the C# event keyword.

At this point, you need to create the Accelerate() method. Recall, the point here is to allow a Car object to send engine-related messages to any subscribed listener. Here is the update:

// 4) Implement the Accelerate() method to invoke the delegate's
// invocation list under the correct circumstances.
public void Accelerate(int delta)
{
// If this car is "dead," send dead message. if (_carIsDead)
{
_listOfHandlers?.Invoke("Sorry, this car is dead...");
}
else
{

CurrentSpeed += delta;
// Is this car "almost dead"?
if (10 == (MaxSpeed - CurrentSpeed))
{
_listOfHandlers?.Invoke("Careful buddy! Gonna blow!");
}
if (CurrentSpeed >= MaxSpeed)
{
_carIsDead = true;
}
else
{
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
}

Notice that you are using the null propagation syntax when attempting to invoke the methods maintained by the listOfHandlers member variable. The reason is that it will be the job of the caller to allocate these objects by calling the RegisterWithCarEngine() helper method. If the caller does not call this method and you attempt to invoke the delegate’s invocation list, you will trigger a NullReferenceException at runtime. Now that you have the delegate infrastructure in place, observe the updates to the Program.cs file, shown here:

using CarDelegate;

Console.WriteLine(" Delegates as event enablers \n");

// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10);

// Now, tell the car which method to call
// when it wants to send us messages.
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent));

// Speed up (this will trigger the events). Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();

// This is the target for incoming events.
static void OnCarEngineEvent(string msg)
{
Console.WriteLine("\n Message From Car Object "); Console.WriteLine("=> {0}", msg); Console.WriteLine("****\n");
}

The code begins by simply making a new Car object. Since you are interested in hearing about the engine events, the next step is to call your custom registration function, RegisterWithCarEngine(). Recall that this method expects to be passed an instance of the nested CarEngineHandler delegate, and as with any delegate, you specify a “method to point to” as a constructor parameter. The trick in this example is that the method in question is located back in the Program.cs file! Again, notice that the OnCarEngineEvent() method is a dead-on match to the related delegate, in that it takes a string as input and returns void.
Consider the output of the current example:

Delegates as event enablers
Speeding up CurrentSpeed = 30
CurrentSpeed = 50
CurrentSpeed = 70

Message From Car Object
=> Careful buddy! Gonna blow!
CurrentSpeed = 90
Message From Car Object ***
=> Sorry, this car is dead...


Enabling Multicasting
Recall that .NET delegates have the built-in ability to multicast. In other words, a delegate object can maintain a list of methods to call, rather than just a single method. When you want to add multiple methods to a delegate object, you simply use the overloaded += operator, rather than a direct assignment. To enable multicasting on the Car class, you could update the RegisterWithCarEngine() method, like so:

public class Car
{
// Now with multicasting support!
// Note we are now using the += operator, not
// the assignment operator (=).
public void RegisterWithCarEngine( CarEngineHandler methodToCall)

{

}
...
}

_listOfHandlers += methodToCall;

When you use the += operator on a delegate object, the compiler resolves this to a call on the static Delegate.Combine() method. In fact, you could call Delegate.Combine() directly; however, the += operator offers a simpler alternative. There is no need to modify your current RegisterWithCarEngine() method, but here is an example of using Delegate.Combine() rather than the += operator:

public void RegisterWithCarEngine( CarEngineHandler methodToCall )
{
if (_listOfHandlers == null)
{
_listOfHandlers = methodToCall;
}
else
{
_listOfHandlers = Delegate.Combine(_listOfHandlers, methodToCall)
as CarEngineHandler;
}
}

In any case, the caller can now register multiple targets for the same callback notification. Here, the second handler prints the incoming message in uppercase, just for display purposes:

Console.WriteLine(" Delegates as event enablers \n");

// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10);

// Register multiple targets for the notifications.
c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent)); c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent2));

// Speed up (this will trigger the events). Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();

// We now have TWO methods that will be called by the Car
// when sending notifications.
static void OnCarEngineEvent(string msg)
{
Console.WriteLine("\n Message From Car Object "); Console.WriteLine("=> {0}", msg); Console.WriteLine("*****\n");
}

static void OnCarEngineEvent2(string msg)
{
Console.WriteLine("=> {0}", msg.ToUpper());
}

Removing Targets from a Delegate’s Invocation List
The Delegate class also defines a static Remove() method that allows a caller to dynamically remove a method from a delegate object’s invocation list. This makes it simple to allow the caller to “unsubscribe” from a given notification at runtime. While you could call Delegate.Remove() directly in code, C# developers can use the -= operator as a convenient shorthand notation. Let’s add a new method to the Car class that allows a caller to remove a method from the invocation list.

public class Car
{
...
public void UnRegisterWithCarEngine(CarEngineHandler methodToCall)
{
_listOfHandlers -= methodToCall;
}
}

With the current updates to the Car class, you could stop receiving the engine notification on the second handler by updating the calling code as follows:

Console.WriteLine(" Delegates as event enablers \n");

// First, make a Car object.
Car c1 = new Car("SlugBug", 100, 10); c1.RegisterWithCarEngine(
new Car.CarEngineHandler(OnCarEngineEvent));

// This time, hold onto the delegate object,
// so we can unregister later.
Car.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2); c1.RegisterWithCarEngine(handler2);

// Speed up (this will trigger the events). Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}

// Unregister from the second handler.
c1.UnRegisterWithCarEngine(handler2);

// We won't see the "uppercase" message anymore! Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}

Console.ReadLine();

One difference in this code is that this time you are creating a Car.CarEngineHandler object and storing it in a local variable so you can use this object to unregister with the notification later. Thus, the second time you speed up the Car object, you no longer see the uppercase version of the incoming message data, as you have removed this target from the delegate’s invocation list.

Method Group Conversion Syntax
In the previous CarDelegate example, you explicitly created instances of the Car.CarEngineHandler delegate object to register and unregister with the engine notifications.
Console.WriteLine(" Delegates as event enablers \n"); Car c1 = new Car("SlugBug", 100, 10);
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));

Car.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2);
c1.RegisterWithCarEngine(handler2);
...

To be sure, if you need to call any of the inherited members of MulticastDelegate or Delegate, manually creating a delegate variable is the most straightforward way of doing so. However, in most cases, you don’t really need to hang onto the delegate object. Rather, you typically need to use the delegate object only to pass in the method name as a constructor parameter.
As a simplification, C# provides a shortcut termed method group conversion. This feature allows you to supply a direct method name, rather than a delegate object, when calling methods that take delegates as arguments.

■ Note as you will see later in this chapter, you can also use method group conversion syntax to simplify how you register with a C# event.

To illustrate, consider the following updates to the Program.cs file, which uses method group conversion to register and unregister from the engine notifications:

...
Console.WriteLine(" Method Group Conversion \n"); Car c2 = new Car();

// Register the simple method name.
c2.RegisterWithCarEngine(OnCarEngineEvent);

Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c2.Accelerate(20);
}

// Unregister the simple method name.
c2.UnRegisterWithCarEngine(OnCarEngineEvent);

// No more notifications! for (int i = 0; i < 6; i++)
{
c2.Accelerate(20);
}

Console.ReadLine();

Notice that you are not directly allocating the associated delegate object but rather simply specifying a method that matches the delegate’s expected signature (a method returning void and taking a single string, in this case). Understand that the C# compiler is still ensuring type safety. Thus, if the OnCarEngineEvent() method did not take a string and return void, you would be issued a compiler error.

Understanding Generic Delegates
In Chapter 10, I mentioned that C# allows you to define generic delegate types. For example, assume you want to define a delegate type that can call any method returning void and receiving a single parameter. If the argument in question may differ, you could model this using a type parameter. To illustrate, consider the following code within a new Console Application project named GenericDelegate:

Console.WriteLine(" Generic Delegates \n");

// Register targets. MyGenericDelegate strTarget =
new MyGenericDelegate(StringTarget); strTarget("Some string data");

//Using the method group conversion syntax MyGenericDelegate intTarget = IntTarget; intTarget(9);
Console.ReadLine();

static void StringTarget(string arg)
{
Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper());
}

static void IntTarget(int arg)
{
Console.WriteLine("++arg is: {0}", ++arg);
}
// This generic delegate can represent any method
// returning void and taking a single parameter of type T. public delegate void MyGenericDelegate(T arg);

Notice that MyGenericDelegate defines a single type parameter that represents the argument to pass to the delegate target. When creating an instance of this type, you are required to specify the value of the type parameter, as well as the name of the method the delegate will invoke. Thus, if you specified a string type, you send a string value to the target method.

// Create an instance of MyGenericDelegate
// with string as the type parameter. MyGenericDelegate strTarget = StringTarget; strTarget("Some string data");

Given the format of the strTarget object, the StringTarget() method must now take a single string as a parameter.

static void StringTarget(string arg)
{
Console.WriteLine(
"arg in uppercase is: {0}", arg.ToUpper());
}

The Generic Action<> and Func<> Delegates
Over the course of this chapter, you have seen that when you want to use delegates to enable callbacks in your applications, you typically follow the steps shown here:
1.Define a custom delegate that matches the format of the method being pointed to.
2.Create an instance of your custom delegate, passing in a method name as a constructor argument.
3.Invoke the method indirectly, via a call to Invoke() on the delegate object.
When you take this approach, you typically end up with several custom delegates that might never be used beyond the current task at hand (e.g., MyGenericDelegate, CarEngineHandler, etc.). While it may certainly be the case that you do indeed need to have a custom, uniquely named delegate type for your project, other times the exact name of the delegate type is irrelevant. In many cases, you simply want “some delegate” that takes a set of arguments and possibly has a return value other than void. In these cases, you can use the framework’s built-in Action<> and Func<> delegate types. To illustrate their usefulness, create a new Console Application project named ActionAndFuncDelegates.
The generic Action<> delegate is defined in the System namespace, and you can use this generic delegate to “point to” a method that takes up to 16 arguments (that ought to be enough!) and returns void. Now recall, because Action<> is a generic delegate, you will need to specify the underlying types of each parameter as well.
Update your Program.cs file to define a new static method that takes three (or so) unique parameters.
Here’s an example:

// This is a target for the Action<> delegate.
static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount)
{
// Set color of console text.
ConsoleColor previous = Console.ForegroundColor; Console.ForegroundColor = txtColor;

for (int i = 0; i < printCount; i++)
{
Console.WriteLine(msg);
}

// Restore color. Console.ForegroundColor = previous;
}

Now, rather than building a custom delegate manually to pass the program’s flow to the
DisplayMessage() method, you can use the out-of-the-box Action<> delegate, as so:

Console.WriteLine(" Fun with Action and Func ");

// Use the Action<> delegate to point to DisplayMessage.
Action<string, ConsoleColor, int> actionTarget = DisplayMessage;
actionTarget("Action Message!", ConsoleColor.Yellow, 5); Console.ReadLine();
As you can see, using the Action<> delegate saves you the bother of defining a custom delegate type. However, recall that the Action<> delegate type can point only to methods that take a void return value. If you want to point to a method that does have a return value (and don’t want to bother writing the custom delegate yourself), you can use Func<>.
The generic Func<> delegate can point to methods that (like Action<>) take up to 16 parameters and a custom return value. To illustrate, add the following new method to the Program.cs file:

// Target for the Func<> delegate. static int Add(int x, int y)
{
return x + y;
}

Earlier in the chapter, I had you build a custom BinaryOp delegate to “point to” addition and subtraction methods. However, you can simplify your efforts using a version of Func<> that takes a total of three type parameters. Be aware that the final type parameter of Func<> is always the return value of the method. Just to solidify that point, assume the Program.cs file also defines the following method:

static string SumToString(int x, int y)
{
return (x + y).ToString();
}

Now, the calling code can call each of these methods, as so:

Func<int, int, int> funcTarget = Add; int result = funcTarget.Invoke(40, 40);
Console.WriteLine("40 + 40 = {0}", result);

Func<int, int, string> funcTarget2 = SumToString; string sum = funcTarget2(90, 300); Console.WriteLine(sum);

In any case, given that Action<> and Func<> can save you the step of manually defining a custom delegate, you might be wondering if you should use them all the time. The answer, like so many aspects of programming, is “it depends.” In many cases, Action<> and Func<> will be the preferred course of action (no pun intended). However, if you need a delegate that has a custom name that you feel helps better capture your problem domain, building a custom delegate is as simple as a single code statement. You’ll see both approaches as you work over the remainder of this text.

■ Note many important .NET apIs make considerable use of Action<> and Func<> delegates, including the parallel programming framework and lINQ (among others).

That wraps up our initial look at the delegate type. Next, let’s move on to the related topic of the C#
event keyword.

Understanding C# Events
Delegates are interesting constructs, in that they enable objects in memory to engage in a two-way conversation. However, working with delegates in the raw can entail the creation of some boilerplate code (defining the delegate, declaring necessary member variables, creating custom registration and unregistration methods to preserve encapsulation, etc.).
Moreover, when you use delegates in the raw as your application’s callback mechanism, if you do not define a class’s delegate member variables as private, the caller will have direct access to the
delegate objects. In this case, the caller could reassign the variable to a new delegate object (effectively deleting the current list of functions to call), and, worse yet, the caller would be able to directly invoke the delegate’s invocation list. To demonstrate this problem, create a new Console Application named PublicDelegateProblem and add the following reworking (and simplification) of the Car class from the previous CarDelegate example:

namespace PublicDelegateProblem; public class Car
{
public delegate void CarEngineHandler(string msgForCaller);

// Now a public member!
public CarEngineHandler ListOfHandlers;

// Just fire out the Exploded notification. public void Accelerate(int delta)
{
if (ListOfHandlers != null)
{
ListOfHandlers("Sorry, this car is dead...");
}
}
}

Notice that you no longer have private delegate member variables encapsulated with custom registration methods. Because these members are indeed public, the caller can directly access the listOfHandlers member variable and reassign this type to new CarEngineHandler objects and invoke the delegate whenever it so chooses.

using PublicDelegateProblem;

Console.WriteLine(" Agh! No Encapsulation! \n");
// Make a Car.
Car myCar = new Car();
// We have direct access to the delegate! myCar.ListOfHandlers = CallWhenExploded; myCar.Accelerate(10);

// We can now assign to a whole new object...
// confusing at best. myCar.ListOfHandlers = CallHereToo; myCar.Accelerate(10);

// The caller can also directly invoke the delegate! myCar.ListOfHandlers.Invoke("hee, hee, hee..."); Console.ReadLine();

static void CallWhenExploded(string msg)
{
Console.WriteLine(msg);
}

static void CallHereToo(string msg)
{
Console.WriteLine(msg);
}

Exposing public delegate members breaks encapsulation, which not only can lead to code that is hard to maintain (and debug) but could also open your application to possible security risks! Here is the output of the current example:

Agh! No Encapsulation! Sorry, this car is dead...
Sorry, this car is dead... hee, hee, hee...

Obviously, you would not want to give other applications the power to change what a delegate is pointing to or to invoke the members without your permission. Given this, it is common practice to declare private delegate member variables.

The C# event Keyword
As a shortcut, so you don’t have to build custom methods to add or remove methods to a delegate’s invocation list, C# provides the event keyword. When the compiler processes the event keyword, you are

automatically provided with registration and un-registration methods, as well as any necessary member variables for your delegate types. These delegate member variables are always declared private, and, therefore, they are not directly exposed from the object firing the event. To be sure, the event keyword can be used to simplify how a custom class sends out notifications to external objects.
Defining an event is a two-step process. First, you need to define a delegate type (or reuse an existing one) that will hold the list of methods to be called when the event is fired. Next, you declare an event (using the C# event keyword) in terms of the related delegate type.
To illustrate the event keyword, create a new console application named CarEvents. In this iteration of the Car class, you will define two events named AboutToBlow and Exploded. These events are associated to a single delegate type named CarEngineHandler. Here are the initial updates to the Car class:

namespace CarEvents; public class Car
{
...
// This delegate works in conjunction with the
// Car's events.
public delegate void CarEngineHandler(string msgForCaller);

// This car can send these events.
public event CarEngineHandler Exploded; public event CarEngineHandler AboutToBlow;
...
}

Sending an event to the caller is as simple as specifying the event by name, along with any required parameters as defined by the associated delegate. To ensure that the caller has indeed registered with the event, you will want to check the event against a null value before invoking the delegate’s method set. With these points in mind, here is the new iteration of the Car’s Accelerate() method:

public void Accelerate(int delta)
{
// If the car is dead, fire Exploded event.
if (_carIsDead)
{
Exploded?.Invoke("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;

// Almost dead?
if (10 == MaxSpeed - CurrentSpeed)
{
AboutToBlow?.Invoke("Careful buddy! Gonna blow!");
}

// Still OK!
if (CurrentSpeed >= MaxSpeed)
{
_carIsDead = true;

}
else
{
Console.WriteLine("CurrentSpeed = {0}", CurrentSpeed);
}
}
}

With this, you have configured the car to send two custom events without having to define custom registration functions or declare delegate member variables. You will see the usage of this new automobile in just a moment, but first let’s check the event architecture in a bit more detail.

Events Under the Hood
When the compiler processes the C# event keyword, it generates two hidden methods, one having an add prefix and the other having a remove prefix. Each prefix is followed by the name of the C# event. For example, the Exploded event results in two hidden methods named addExploded() and remove
Exploded(). If you were to check out the CIL instructions behind add_AboutToBlow(), you would find a call to the Delegate.Combine() method. Consider the partial CIL code:

.method public hidebysig specialname instance void add_AboutToBlow(
class [System.Runtime]System.EventHandler`1 'value') cil managed
{
...
IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System. Delegate::Combine(class [System.Runtime]System.Delegate, class [System.Runtime]System. Delegate)
...
} // end of method Car::add_AboutToBlow

As you would expect, remove_AboutToBlow() will call Delegate.Remove() on your behalf.

.method public hidebysig specialname instance void remove_AboutToBlow (
class [System.Runtime]System.EventHandler`1 'value') cil managed
{
...

...
}

IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System. Delegate::Remove(class [System.Runtime]System.Delegate, class [System.Runtime]System. Delegate)

Finally, the CIL code representing the event itself uses the .addon and .removeon directives to map the names of the correct add_XXX() and remove_XXX() methods to invoke.

.event class [System.Runtime]System.EventHandler`1 AboutToBlow
{
.addon instance void CarEvents.Car::add_AboutToBlow(

class [System.Runtime]System.EventHandler1) .removeon instance void CarEvents.Car::remove_AboutToBlow( class [System.Runtime]System.EventHandler1)
} // end of event Car::AboutToBlow

Now that you understand how to build a class that can send C# events (and are aware that events are little more than a typing time-saver), the next big question is how to listen to the incoming events on the caller’s side.

Listening to Incoming Events
C# events also simplify the act of registering the caller-side event handlers. Rather than having to specify custom helper methods, the caller simply uses the += and -= operators directly (which triggers the correct add_XXX() or remove_XXX() method in the background). When you want to register with an event, follow the pattern shown here:

// NameOfObject.NameOfEvent +=
// new RelatedDelegate(functionToCall);
//
Car.CarEngineHandler d =
new Car.CarEngineHandler(CarExplodedEventHandler); myCar.Exploded += d;

When you want to detach from a source of events, use the -= operator, using the following pattern:

// NameOfObject.NameOfEvent -=
// new RelatedDelegate(functionToCall);
//
myCar.Exploded -= d;

Note that you can also use the method group conversion syntax with events as well:

Car.CarEngineHandler d = CarExplodedEventHandler; myCar.Exploded += d;

Given these very predictable patterns, here is the refactored calling code, now using the C# event registration syntax:

Console.WriteLine(" Fun with Events \n"); Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers. c1.AboutToBlow += CarIsAlmostDoomed; c1.AboutToBlow += CarAboutToBlow;

Car.CarEngineHandler d = CarExploded; c1.Exploded += d;

Console.WriteLine(" Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}

// Remove CarExploded method
// from invocation list. c1.Exploded -= d;

Console.WriteLine("\n Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();

static void CarAboutToBlow(string msg)
{
Console.WriteLine(msg);
}

static void CarIsAlmostDoomed(string msg)
{
Console.WriteLine("=> Critical Message from Car: {0}", msg);
}

static void CarExploded(string msg)
{
Console.WriteLine(msg);
}

Simplifying Event Registration Using Visual Studio
Visual Studio helps with the process of registering event handlers. When you apply the += syntax during event registration, you will find an IntelliSense window displayed, inviting you to hit the Tab key to autocomplete the associated delegate instance (see Figure 12-1), which is captured using method group conversion syntax.

Figure 12-1. Delegate selection IntelliSense

After you hit the Tab key, the IDE will generate the new method automatically, as shown in Figure 12-2.

Figure 12-2. Delegate target format IntelliSense

Note the stub code is in the correct format of the delegate target (note that this method has been declared static because the event was registered within a static method).

static void NewCar_AboutToBlow(string msg)
{
throw new NotImplementedException();
}

IntelliSense is available to all .NET events, your custom events, and all the events in the base class libraries. This IDE feature is a massive time-saver, given that it saves you from having to search the help system to figure out both the correct delegate to use with an event and the format of the delegate target method.

Creating Custom Event Arguments
Truth be told, there is one final enhancement you could make to the current iteration of the Car class that mirrors Microsoft’s recommended event pattern. As you begin to explore the events sent by a given type in the base class libraries, you will find that the first parameter of the underlying delegate is a System.Object, while the second parameter is a descendant of System.EventArgs.

The System.Object argument represents a reference to the object that sent the event (such as the Car), while the second parameter represents information regarding the event at hand. The System.EventArgs base class represents an event that is not sending any custom information.

public class EventArgs
{
public static readonly EventArgs Empty; public EventArgs();
}

For simple events, you can pass an instance of EventArgs directly. However, when you want to pass along custom data, you should build a suitable class deriving from EventArgs. For this example, assume you have a class named CarEventArgs, which maintains a string representing the message sent to the receiver.

namespace CarEvents;
public class CarEventArgs : EventArgs
{
public readonly string msg;
public CarEventArgs(string message)
{
msg = message;
}
}

With this, you would now update the CarEngineHandler delegate type definition as follows (the events would be unchanged):

public class Car
{
public delegate void CarEngineHandler(object sender, CarEventArgs e);
...
}

Here, when firing the events from within the Accelerate() method, you would now need to supply a reference to the current Car (via the this keyword) and an instance of the CarEventArgs type. For example, consider the following partial update:

public void Accelerate(int delta)
{
// If the car is dead, fire Exploded event. if (carIsDead)
{
Exploded?.Invoke(this, new CarEventArgs("Sorry, this car is dead..."));
}
...
}

On the caller’s side, all you would need to do is update your event handlers to receive the incoming parameters and obtain the message via the read-only field. Here’s an example:

static void CarAboutToBlow(object sender, CarEventArgs e)

{
Console.WriteLine($"{sender} says: {e.msg}");
}

If the receiver wants to interact with the object that sent the event, you can explicitly cast the System.
Object. From this reference, you can make use of any public member of the object that sent the event notification.

static void CarAboutToBlow(object sender, CarEventArgs e)
{
// Just to be safe, perform a
// runtime check before casting. if (sender is Car c)
{
Console.WriteLine(
$"Critical Message from {c.PetName}: {e.msg}");
}
}

The Generic EventHandler Delegate
Given that so many custom delegates take an object as the first parameter and an EventArgs descendant as the second, you could further streamline the previous example by using the generic EventHandler type, where T is your custom EventArgs type. Consider the following update to the Car type (notice how you no longer need to define a custom delegate type at all):

public class Car
{
...
public event EventHandler Exploded; public event EventHandler AboutToBlow;
}

The calling code could then use EventHandler anywhere you previously specified
CarEventHandler (or, once again, use method group conversion).

Console.WriteLine(" Prim and Proper Events \n");

// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers. c1.AboutToBlow += CarIsAlmostDoomed; c1.AboutToBlow += CarAboutToBlow;

EventHandler d = CarExploded; c1.Exploded += d;
...

Great! At this point, you have seen the core aspects of working with delegates and events in the C# language. While you could use this information for just about all your callback needs, you will wrap up this chapter with a look at some final simplifications, specifically anonymous methods and lambda expressions.

Understanding C# Anonymous Methods
As you have seen, when a caller wants to listen to incoming events, it must define a custom method in a class (or structure) that matches the signature of the associated delegate. Here’s an example:

SomeType t = new SomeType();

// Assume "SomeDelegate" can point to methods taking no
// args and returning void.
t.SomeEvent += new SomeDelegate(MyEventHandler);

// Typically only called by the SomeDelegate object. static void MyEventHandler()
{
// Do something when event is fired.
}

When you think about it, however, methods such as MyEventHandler() are seldom intended to be called by any part of the program other than the invoking delegate. As far as productivity is concerned, it is a bit of a bother (though in no way a showstopper) to manually define a separate method to be called by the delegate object.
To address this point, it is possible to associate an event directly to a block of code statements at the time of event registration. Formally, such code is termed an anonymous method. To illustrate the syntax, first create a new Console Application named AnonymousMethods, and copy the Car.cs and CarEventArgs.
cs classes from the CarEvents project into the new project (making sure to change their namespaces to AnonymousMethods). Update the Program.cs file’s code to match the following, which handles the events sent from the Car class using anonymous methods, rather than specifically named event handlers:

using AnonymousMethods;

Console.WriteLine(" Anonymous Methods \n"); Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers as anonymous methods. c1.AboutToBlow += delegate
{
Console.WriteLine("Eek! Going too fast!");
};

c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
Console.WriteLine("Message from Car: {0}", e.msg);
};

c1.Exploded += delegate(object sender, CarEventArgs e)
{

Console.WriteLine("Fatal Message from Car: {0}", e.msg);
};

// This will eventually trigger the events. for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();

■ Note The final curly bracket of an anonymous method must be terminated by a semicolon. If you fail to do so, you are issued a compilation error.

Again, notice that the calling code no longer needs to define specific static event handlers such as CarAboutToBlow() or CarExploded(). Rather, the unnamed (aka anonymous) methods are defined inline at the time the caller is handling the event using the += syntax. The basic syntax of an anonymous method matches the following pseudocode:

SomeType t = new SomeType();
t.SomeEvent += delegate (optionallySpecifiedDelegateArgs)
{ / statements / };

When handling the first AboutToBlow event within the previous code sample, notice that you are not specifying the arguments passed from the delegate.

c1.AboutToBlow += delegate
{
Console.WriteLine("Eek! Going too fast!");
};

Strictly speaking, you are not required to receive the incoming arguments sent by a specific event.
However, if you want to make use of the possible incoming arguments, you will need to specify the parameters prototyped by the delegate type (as shown in the second handling of the AboutToBlow and Exploded events). Here’s an example:

c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
Console.WriteLine("Critical Message from Car: {0}", e.msg);
};

Accessing Local Variables
Anonymous methods are interesting, in that they can access the local variables of the method that defines them. Formally speaking, such variables are termed outer variables of the anonymous method. The following important points about the interaction between an anonymous method scope and the scope of the defining method should be mentioned:
• An anonymous method cannot access ref or out parameters of the defining method.

•An anonymous method cannot have a local variable with the same name as a local variable in the outer method.
•An anonymous method can access instance variables (or static variables, as appropriate) in the outer class scope.
•An anonymous method can declare local variables with the same name as outer class member variables (the local variables have a distinct scope and hide the outer class member variables).
Assume your top-level statements define a local integer named aboutToBlowCounter. Within the anonymous methods that handle the AboutToBlow event, you will increment this counter by one and print out the tally before the statements complete.

Console.WriteLine(" Anonymous Methods \n"); int aboutToBlowCounter = 0;

// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers as anonymous methods. c1.AboutToBlow += delegate
{
aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};

c1.AboutToBlow += delegate(object sender, CarEventArgs e)
{
aboutToBlowCounter++;
Console.WriteLine("Critical Message from Car: {0}", e.msg);
};
...
// This will eventually trigger the events. for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}

Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter);
Console.ReadLine();

After you run this updated code, you will find the final Console.WriteLine() reports the AboutToBlow
event was fired twice.

Using static with Anonymous Methods (New 9.0)
The previous example demonstrated anonymous methods interacting with variables declared outside of the scope of the method itself. While this might be what you intend, it breaks encapsulation and could introduce unintended side effects into your program. Recall from Chapter 4 that local functions can be isolated from the containing code by setting them as static, as in the following example:

static int AddWrapperWithStatic(int x, int y)
{
//Do some validation here return Add(x,y);
static int Add(int x, int y)
{
return x + y;
}
}

New in C# 9.0, anonymous methods can also be marked as static to preserve encapsulation and ensure that the method cannot introduce any side effects into the containing code. For example, see the updated anonymous method here:

c1.AboutToBlow += static delegate
{
//This causes a compile error because it is marked static aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};

The preceding code will not compile due to the anonymous methods attempting to access the variable declared outside its scope. To remove the compile error, comment out (or remove) the line that updates the counter:

c1.AboutToBlow += static delegate
{
//aboutToBlowCounter++; Console.WriteLine("Eek! Going too fast!");
};

Discards with Anonymous Methods (New 9.0)
Discards, introduced in Chapter 3, have been updated in C# 9.0 for use as input parameters for anonymous methods, with a catch. Because the underscore (_) was a legal variable identifier in previous versions of C#, there must be two or more discards used with the anonymous method to be treated as discards.
For example, the following code created a delegate for a Func that takes two integers and returns another. This implementation ignores any variables passed in and returns 42:

Console.WriteLine(" Discards with Anonymous Methods ");

Func<int,int,int> constant = delegate (int , int ) {return 42;}; Console.WriteLine("constant(3,4)={0}",constant(3,4));

Understanding Lambda Expressions
To conclude your look at the .NET event architecture, you will examine C# lambda expressions. As just explained, C# supports the ability to handle events “inline” by assigning a block of code statements directly to an event using anonymous methods, rather than building a stand-alone method to be called by the underlying delegate. Lambda expressions are nothing more than a concise way to author anonymous methods and ultimately simplify how you work with the .NET delegate type.
To set the stage for your examination of lambda expressions, create a new Console Application project named LambdaExpressions. To begin, consider the FindAll() method of the generic List class. This method can be called when you need to extract a subset of items from the collection and is prototyped
like so:

// Method of the System.Collections.Generic.List public List FindAll(Predicate match)

As you can see, this method returns a new List that represents the subset of data. Also notice that the sole parameter to FindAll() is a generic delegate of type System.Predicate. This delegate type can point to any method returning a bool and takes a single type parameter as the only input parameter.

// This delegate is used by FindAll() method
// to extract out the subset.
public delegate bool Predicate(T obj);

When you call FindAll(), each item in the List is passed to the method pointed to by the Predicate object. The implementation of said method will perform some calculations to see whether the incoming data matches the necessary criteria and will return true or false. If this method returns true, the item will be added to the new List that represents the subset (got all that?).
Before you see how lambda expressions can simplify working with FindAll(), let’s work the problem out in longhand notation, using the delegate objects directly. Add a method (named
TraditionalDelegateSyntax()) within your Program.cs file that interacts with the System.Predicate
type to discover the even numbers in a List of integers.

using LambdaExpressions;

Console.WriteLine(" Fun with Lambdas \n"); TraditionalDelegateSyntax();
Console.ReadLine();

static void TraditionalDelegateSyntax()
{
// Make a list of integers.
List list = new List(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Call FindAll() using traditional delegate syntax.
Predicate callback = IsEvenNumber; List evenNumbers = list.FindAll(callback);

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{

Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

// Target for the Predicate<> delegate.
static bool IsEvenNumber(int i)
{
// Is it an even number? return (i % 2) == 0;
}

Here, you have a method (IsEvenNumber()) that oversees testing the incoming integer parameter to see whether it is even or odd via the C# modulo operator, %. If you execute your application, you will find the numbers 20, 4, 8, and 44 print to the console.
While this traditional approach to working with delegates behaves as expected, the IsEvenNumber() method is invoked only in limited circumstances—specifically when you call FindAll(), which leaves you with the baggage of a full method definition. While you could make this a local function, if you were to instead use an anonymous method, your code would clean up considerably. Consider the following new method of the Program.cs file:

static void AnonymousMethodSyntax()
{
// Make a list of integers.
List list = new List(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Now, use an anonymous method.
List evenNumbers =
list.FindAll(delegate(int i) { return (i % 2) == 0; } );

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

In this case, rather than directly creating a Predicate delegate object and then authoring a stand- alone method, you can inline a method anonymously. While this is a step in the right direction, you are still required to use the delegate keyword (or a strongly typed Predicate), and you must ensure that the parameter list is a dead-on match.

List evenNumbers = list.FindAll( delegate(int i)
{
return (i % 2) == 0;
}
);

Lambda expressions can be used to simplify the call to FindAll() even more. When you use lambda syntax, there is no trace of the underlying delegate object whatsoever. Consider the following new method to the Program.cs file:

static void LambdaExpressionSyntax()
{
// Make a list of integers. List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Now, use a C# lambda expression.
List evenNumbers = list.FindAll(i => (i % 2) == 0);

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

In this case, notice the rather strange statement of code passed into the FindAll() method, which is in fact a lambda expression. In this iteration of the example, there is no trace whatsoever of the Predicate delegate (or the delegate keyword, for that matter). All you have specified is the lambda expression.

i => (i % 2) == 0

Before I break this syntax down, first understand that lambda expressions can be used anywhere you would have used an anonymous method or a strongly typed delegate (typically with far fewer keystrokes). Under the hood, the C# compiler translates the expression into a standard anonymous method making use of the Predicate delegate type (which can be verified using ildasm.exe or reflector.exe). Specifically, the following code statement:

// This lambda expression...
List evenNumbers = list.FindAll(i => (i % 2) == 0);

is compiled into the following approximate C# code:

// ...becomes this anonymous method.
List evenNumbers = list.FindAll(delegate (int i)
{
return (i % 2) == 0;
});

Dissecting a Lambda Expression
A lambda expression is written by first defining a parameter list, followed by the => token (C#’s token for the lambda operator found in the lambda calculus), followed by a set of statements (or a single statement) that will process these arguments. From a high level, a lambda expression can be understood as follows:

ArgumentsToProcess => StatementsToProcessThem

Within the LambdaExpressionSyntax() method, things break down like so:

// "i" is our parameter list.
// "(i % 2) == 0" is our statement set to process "i". List evenNumbers = list.FindAll(i => (i % 2) == 0);

The parameters of a lambda expression can be explicitly or implicitly typed. Currently, the underlying data type representing the i parameter (an integer) is determined implicitly. The compiler can figure out that i is an integer based on the context of the overall lambda expression and the underlying delegate.
However, it is also possible to explicitly define the type of each parameter in the expression by wrapping the data type and variable name in a pair of parentheses, as follows:

// Now, explicitly state the parameter type.
List evenNumbers = list.FindAll((int i) => (i % 2) == 0);

As you have seen, if a lambda expression has a single, implicitly typed parameter, the parentheses may be omitted from the parameter list. If you want to be consistent regarding your use of lambda parameters, you can always wrap the parameter list within parentheses, leaving you with this expression:
List evenNumbers = list.FindAll((i) => (i % 2) == 0);

Finally, notice that currently the expression has not been wrapped in parentheses (you have of course wrapped the modulo statement to ensure it is executed first before the test for equality). Lambda expressions do allow for the statement to be wrapped as follows:

// Now, wrap the expression as well.
List evenNumbers = list.FindAll((i) => ((i % 2) == 0));

Now that you have seen the various ways to build a lambda expression, how can you read this lambda statement in human-friendly terms? Leaving the raw mathematics behind, the following explanation fits the bill:

// My list of parameters (in this case, a single integer named i)
// will be processed by the expression (i % 2) == 0. List evenNumbers = list.FindAll((i) => ((i % 2) == 0));

Processing Arguments Within Multiple Statements
The first lambda expression was a single statement that ultimately evaluated to a Boolean. However, as you know, many delegate targets must perform several code statements. For this reason, C# allows you to build lambda expressions containing multiple statements by specifying a code block using the standard curly braces. Consider the following example update to the LambdaExpressionSyntax() method:

static void LambdaExpressionSyntax()
{
// Make a list of integers. List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// Now process each argument within a group of
// code statements.
List evenNumbers = list.FindAll((i) =>

{
Console.WriteLine("value of i is currently: {0}", i); bool isEven = ((i % 2) == 0);
return isEven;
});

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

In this case, the parameter list (again, a single integer named i) is being processed by a set of code statements. Beyond the calls to Console.WriteLine(), the modulo statement has been broken into two code statements for increased readability. Assuming each of the methods you’ve looked at in this section is called from within your top-level statements:

Console.WriteLine(" Fun with Lambdas \n"); TraditionalDelegateSyntax(); AnonymousMethodSyntax();
Console.WriteLine(); LambdaExpressionSyntax(); Console.ReadLine();

you will find the following output:

Fun with Lambdas 4
4

4

Lambda Expressions with Multiple (or Zero) Parameters
The lambda expressions you have seen in this chapter so far processed a single parameter. This is not a requirement, however, as a lambda expression may process multiple arguments (or none). To illustrate the first scenario of multiple arguments, add the following incarnation of the SimpleMath type:

public class SimpleMath

{
public delegate void MathMessage(string msg, int result); private MathMessage _mmDelegate;

public void SetMathHandler(MathMessage target)
{
_mmDelegate = target;
}

public void Add(int x, int y)
{
_mmDelegate?.Invoke("Adding has completed!", x + y);
}
}

Notice that the MathMessage delegate type is expecting two parameters. To represent them as a lambda expression, the code might be written as follows:

// Register with delegate as a lambda expression. SimpleMath m = new SimpleMath(); m.SetMathHandler((msg, result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});

// This will execute the lambda expression. m.Add(10, 10);
Console.ReadLine();

Here, you are leveraging type inference, as the two parameters have not been strongly typed for simplicity. However, you could call SetMathHandler(), as follows:

m.SetMathHandler((string msg, int result) =>
{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});

Finally, if you are using a lambda expression to interact with a delegate taking no parameters at all, you may do so by supplying a pair of empty parentheses as the parameter. Thus, assuming you have defined the following delegate type:

public delegate string VerySimpleDelegate();

you could handle the result of the invocation as follows:

// Prints "Enjoy your string!" to the console.
VerySimpleDelegate d = new VerySimpleDelegate( () => {return "Enjoy your string!";} ); Console.WriteLine(d());

Using the new expression syntax, the previous line can be written like this:

VerySimpleDelegate d2 =
new VerySimpleDelegate(() => "Enjoy your string!");

which can also be shortened to this:

VerySimpleDelegate d3 = () => "Enjoy your string!";

Using static with Lambda Expressions (New 9.0)
Since lambda expressions are shorthand for delegates, it is understandable that lambda also support the static keyword (with C# 9.0) as well as discards (covered in the next section). Add the following to your top- level statements:

var outerVariable = 0;

Func<int, int, bool> DoWork = (x,y) =>
{
outerVariable++; return true;
};
DoWork(3,4);
Console.WriteLine("Outer variable now = {0}", outerVariable);

When this code is executed, it outputs the following:

Fun with Lambdas Outer variable now = 1
If you update the lambda to static, you will receive a compile error since the expression is trying to update a variable declared in an outer scope.

var outerVariable = 0;

Func<int, int, bool> DoWork = static (x,y) =>
{
//Compile error since it’s accessing an outer variable
//outerVariable++; return true;
};

Discards with Lambda Expressions (New 9.0)
As with delegates (and C# 9.0), input variables to a lambda expression can be replaced with discards if the input variables aren’t needed. The same catch applies as with delegates. Since the underscore (_) was a legal variable identifier in previous versions of C#, they must be two or more discards used with the lambda expression.

var outerVariable = 0;

Func<int, int, bool> DoWork = (x,y) =>

{
outerVariable++; return true;
};
DoWork(,);
Console.WriteLine("Outer variable now = {0}", outerVariable);

Retrofitting the CarEvents Example Using Lambda Expressions
Given that the whole reason for lambda expressions is to provide a clean, concise manner to define an anonymous method (and therefore indirectly a manner to simplify working with delegates), let’s retrofit the CarEventArgs project created earlier in this chapter. Here is a simplified version of that project’s Program.cs file, which makes use of lambda expression syntax (rather than the raw delegates) to hook into each event sent from the Car object:

using CarEventsWithLambdas;

Console.WriteLine(" More Fun with Lambdas \n");

// Make a car as usual.
Car c1 = new Car("SlugBug", 100, 10);

// Hook into events with lambdas! c1.AboutToBlow += (sender, e)
=> { Console.WriteLine(e.msg);};
c1.Exploded += (sender, e) => { Console.WriteLine(e.msg); };

// Speed up (this will generate the events). Console.WriteLine("\n Speeding up "); for (int i = 0; i < 6; i++)
{
c1.Accelerate(20);
}
Console.ReadLine();

Lambdas and Expression-Bodied Members (Updated 7.0)
Now that you understand lambda expressions and how they work, it should be much clearer how expression-bodied members work under the covers. As mentioned in Chapter 4, as of C# 6, it is permissible to use the => operator to simplify member implementations. Specifically, if you have a method or property (in addition to a custom operator or conversion routine; see Chapter 11) that consists of exactly a single line of code in the implementation, you are not required to define a scope via curly bracket. You can instead leverage the lambda operator and write an expression-bodied member. In C# 7, you can also use this syntax for class constructors, finalizers (covered in Chapter 9), and get and set accessors on property members.

Be aware, however, this new shortened syntax can be used anywhere at all, even when your code has nothing to do with delegates or events. So, for example, if you were to build a trivial class to add two numbers, you might write the following:

class SimpleMath
{
public int Add(int x, int y)
{
return x + y;
}

public void PrintSum(int x, int y)
{
Console.WriteLine(x + y);
}
}

Alternatively, you could now write code like the following:

class SimpleMath
{
public int Add(int x, int y) => x + y;
public void PrintSum(int x, int y) => Console.WriteLine(x + y);
}

Ideally, at this point, you can see the overall role of lambda expressions and understand how they provide a “functional manner” to work with anonymous methods and delegate types. Although the lambda operator (=>) might take a bit to get used to, always remember a lambda expression can be broken down to the following simple equation:

ArgumentsToProcess =>
{
//StatementsToProcessThem
}

Or, if using the => operator to implement a single-line type member, it would be like this:

TypeMember => SingleCodeStatement

It is worth pointing out that the LINQ programming model also makes substantial use of lambda expressions to help simplify your coding efforts. You will examine LINQ beginning in Chapter 13.

Summary
In this chapter, you examined several ways in which multiple objects can partake in a bidirectional conversation. First, you looked at the C# delegate keyword, which is used to indirectly construct a class derived from System.MulticastDelegate. As you saw, a delegate object maintains the method to call when told to do so.

You then examined the C# event keyword, which, when used in conjunction with a delegate type, can simplify the process of sending your event notifications to waiting callers. As shown via the resulting CIL, the
.NET event model maps to hidden calls on the System.Delegate/System.MulticastDelegate types. In this light, the C# event keyword is purely optional, in that it simply saves you some typing time. As well, you have seen that the C# 6.0 null conditional operator simplifies how you safely fire events to any interested party.
This chapter also explored a C# language feature termed anonymous methods. Using this syntactic construct, you can directly associate a block of code statements to a given event. As you have seen, anonymous methods are free to ignore the parameters sent by the event and have access to the “outer variables” of the defining method. You also examined a simplified way to register events using method group conversion.
Finally, you wrapped things up by looking at the C# lambda operator, =>. As shown, this syntax is a great shorthand notation for authoring anonymous methods, where a stack of arguments can be passed into a group of statements for processing. Any method in the .NET platform that takes a delegate object as an argument can be substituted with a related lambda expression, which will typically simplify your code base quite a bit.