7 Using dependency injection to manage services

This chapter covers

  • Understanding the role of dependency injection
  • Examining dependency injection in ASP.NET Core
  • Creating and using your own services
  • Managing service lifetime

Dependency injection (DI) is a software engineering technique included as a feature in many web frameworks these days. Its raison d’être is enabling loose coupling between software components, resulting in code that is less brittle, more adaptable to change, easier to maintain, and easier to test. If you have worked with dependency injection before, all of this will feel familiar to you. In case dependency injection is a new concept to you, this chapter will help by explaining what it is and why you should care.

DI is at the heart of ASP.NET Core. The entire framework uses the built-in DI feature to manage its own dependencies, or services. Services are globally registered within a container when the application starts up and then provided by the container when needed by consumers. You encountered the main entry point to the service container when you looked at Program.cs in chapter 2. It is accessed via the Services property of the WebApplicationBuilder. You will recall that services that comprise the Razor Pages framework are registered with the container via the AddRazorPages method:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();

In addition to the framework’s use of its container, you are encouraged to use it to register your own services, so they can be injected into consumers by the container, which will take responsibility for managing the lifetime (creation and destruction) of those services on your behalf. A key point to make here is that you are not required to use DI for your own services. However, you should at least have an understanding about how DI works in a Razor Pages application, so you can customize framework services as needed.

By the end of this chapter, you will understand what dependency injection is, how it is managed within a Razor Pages application, and the benefits of using DI for your own services. You will also have a clearer idea of what a service is in the context of a Razor Pages application. You will have created some services and registered them with the dependency injection container, so they are available throughout your application.

Services need to have their lifetime managed by the DI container, so your application doesn’t run out of memory, for example. Neither should you instantiate services unnecessarily, especially if only one single instance is required by the application. In this chapter, you will learn the different lifetimes available and how to choose the correct one when registering your services.

7.1 The reason for dependency injection

Before we look at the fundamentals of dependency injection in ASP.NET Core, we need to be clear about the nature of the problem it solves. That discussion involves the use of terms the software engineering community has assimilated to describe principles and techniques. Many of the terms are quite abstract and are used to describe abstract ideas. Consequently, they can be difficult to grasp, especially for literal thinkers.

I have already used the term loose coupling as one of the primary targets a software engineer should strive for when designing a system. I’ve mentioned that this loose coupling should occur between components. Before looking at the nature of a component, I want to take a step back and look at the bigger picture in terms of software engineering design principles.

7.1.1 Single-responsibility principle

A key principle you should consider when designing a system is the single-responsibility principle (SRP). This is the S in SOLID, a set of principles designed to make software more understandable, flexible, and maintainable. Fundamentally, SRP states that any component, module, or service within an application should have only one reason to change: its single responsibility needs to change for some business reason. If you consider a PageModel class with this principle in mind, you can see its responsibility is to process the HTTP request for its page. Therefore, the only circumstances under which you should need to change any aspect of the code in the PageModel is if the logic required for processing the request should change.

If you look at the PageModel for the property manager’s Create page you put together in the last chapter, you can see this principle is violated. The PageModel has two responsibilities: to process the request and to generate data for the SelectList in the form of a collection of cities (figure 7.1).

CH07_F01_Brind

Figure 7.1 The PageModel currently has two responsibilities.

In the next chapter, we will look at using databases in Razor Pages applications. You will want to change how the city options are generated by retrieving them from the database. That change has nothing to do with the primary role of a PageModel class—processing a request. However, as things are currently designed, the move to a database will require us to delve into the CreateModel class to alter the GetCityOptions method. In other words, a change of data access strategy currently provides an additional reason for changing the PageModel class. If you are to comply with SRP, you need to move the logic that generates the city data into its own component with the single responsibility of managing data for the City entity.

Think back to the don’t repeat yourself (DRY) principle discussed in chapter 3, which encourages you to minimize code duplication. Each piece of logic should only have one representation in the system, the principle states. We have fallen foul of this principle too. We have code in the home page that generates cities for the list box example, effectively duplicating the GetCityOptions code just discussed in the CreateModel. This code should be centralized, and once again, moving it to its own component will address the fault.

Let’s start by fixing this. You will build a component with the responsibility of generating a collection of City objects and making that available to any part of the application that needs it. Then you will use that component in the PageModel class to provide the source data for one of the select lists you have built so far.

Start by adding a new folder named Services to the root of the application. Within this, add a new C# class named SimpleCityService.cs. The content of the file is provided in the following listing.

Listing 7.1 The SimpleCityService code

using CityBreaks.Models;
 
namespace CityBreaks.Services
{
    public class SimpleCityService
    {   
        public Task<List<City>> GetAllAsync()
        {
            return Task.FromResult(Cities);
        }
 
        private readonly List<City> Cities = new()
        {
            new City { Id = 1, Name = "Amsterdam", Country = new Country { 
                Id = 5, CountryName = "Holland", CountryCode = "nl" 
            } },
            new City { Id = 2, Name = "Barcelona", Country = new Country { 
                Id = 7, CountryName = "Spain", CountryCode = "es" 
            } },
            new City { Id = 3, Name = "Berlin", Country = new Country { 
                Id = 4, CountryName = "Germany", CountryCode = "de" 
            } },
            new City { Id = 4, Name = "Copenhagen", Country = new Country { 
                Id = 2, CountryName = "Denmark", CountryCode = "dk" 
            } },
            new City { Id = 5, Name = "Dubrovnik", Country = new Country { 
                Id = 1, CountryName = "Croatia", CountryCode = "hr" 
            } },
            new City { Id = 6, Name = "Edinburgh", Country = new Country { 
                Id = 8, CountryName = "United Kingdom", CountryCode = "gb" 
            } },
            new City { Id = 7, Name = "London", Country = new Country { 
                Id = 8, CountryName = "United Kingdom", CountryCode = "gb" 
            } },
            new City { Id = 8, Name = "Madrid", Country = new Country { 
                Id = 7, CountryName = "Spain", CountryCode = "es"
            } },
            new City { Id = 9, Name = "New York", Country = new Country { 
                Id = 9, CountryName = "United States", CountryCode = "us" 
            } },
            new City { Id = 10, Name = "Paris", Country = new Country { 
                Id = 3, CountryName = "France", CountryCode = "fr" 
            } },
            new City { Id = 11, Name = "Rome", Country = new Country { 
                Id = 6, CountryName = "Italy", CountryCode = "it" 
            } },
            new City { Id = 12, Name = "Venice", Country = new Country {
                Id = 6, CountryName = "Italy", CountryCode = "it" 
            } }
        };
    }
}

It really is a simple city service. All this code does is generate a collection of cities and the country they belong to and then make them available via a public method called GetAllAsync. You’ve included some unique identifiers that look like relational database primary keys. You might use code like this for a proof of concept application or as a test double (a replacement for a relational database) for unit testing. Or indeed, you could use code like this as part of a demonstration application for learning purposes if you ever write a book! The only slightly odd thing here is that the GetAllAsync method returns a Task<List<City>> rather than just a List<City>. This is because you will be migrating to using a database in the next chapter, so you want to emulate database calls, which are typically executed asynchronously within an ASP.NET Core application. I discuss this in more detail in the next chapter.

Now that you have centralized the creation of cities, you can use your new component in the PropertyManager Create.cshtml.cs file. Open that, and change the existing handler methods and GetCityOptions method, leaving the various page properties as they are. You will also need to add a using directive for CityBreaks.Services.

Listing 7.2 Revised Create Property page using the new SimpleCityService

public async Task OnGetAsync()                                          
{    
    Cities = await GetCityOptions();                                    
}    
 
public async Task OnPostAsync()                                         
{    
    Cities = await GetCityOptions();                                    
    if (ModelState.IsValid)
    {
        var city = Cities.First(o => o.Value == SelectedCity.ToString());
        Message = $"You selected {city.Text} with value of {SelectedCity}";
    }
}
 
private async Task<SelectList> GetCityOptions()                         
{
    var service = new SimpleCityService();                              
    var cities = await service.GetAllAsync();                           
    return new SelectList(cities, nameof(City.Id), 
     nameof(City.Name), null, "Country.CountryName");
}

You amend the handler methods and the GetCityOptions methods to make them asynchronous.

Obtain the data from the SimpleCityService class instead of generating it in the PageModel.

Aside from the conversion to using asynchronous methods, the only real change here is that the responsibility for generating the collection of cities is no longer that of the CreateModel class. That job has been delegated to the new class: the SimpleCityService. The CreateModel class depends on the SimpleCityService for the data, making the SimpleCityService a dependency of the CreateModel class.

7.1.2 Loose coupling

I believe it was Steve Smith, aka “Ardalis,” a well-known ASP.NET speaker, author, and trainer, who first coined the phrase “new is glue” (https://ardalis.com/new-is-glue/). He suggests that whenever you use the new keyword in C# code, you consider whether you are creating a tight coupling between a consumer (a “high-level module”) and its dependencies (“low-level modules”) and, if so, what ramifications that might have in the longer term. In this example, while shifting the logic for providing city data into its own component, you have effectively glued your SimpleCityService component to the CreateModel class. The relationship between these actors is shown in figure 7.2.

CH07_F02_Brind

Figure 7.2 The SimpleCityServices is glued (tightly coupled) to the Create-Model class.

You are in breach of a software engineering principle here, the explicit dependencies principle, which states that “methods and classes should explicitly require (typically through method parameters or constructor parameters) any collaborating objects they need in order to function correctly” (http://mng.bz/9Vzj). Your collaborating object, the SimpleCityService, is an implicit dependency of the CreateModel class because it is only apparent that the CreateModel class depends on the SimpleCityService when you look at the source code of the consuming class. Implicit dependencies are to be avoided. They are difficult to test and make the consumer (the CreateModel) more brittle and resistant to change.

If you want to change the implementation of the data provider to another one, say that gets data from a database, you will have to go through all the places in the code where you call new SimpleCityService() and change it to reference your alternative implementation. And you will be changing the implementation in the next chapter. You might think using your development tool’s Find and Replace feature makes this a relatively painless exercise, but that’s not a sustainable way to build applications, especially when there are better options available for swapping implementations, which we will look at next.

7.1.3 Dependency inversion

So how do you achieve loose coupling? How can you redesign the consumer of your components or services, so they are no longer tightly coupled to a specific, or concrete, implementation? One solution is to rely on abstractions, rather than specific implementations. This approach is known as the dependency inversion principle (DIP), which is the D in the SOLID acronym. Dependency inversion is also known as inversion of control (IoC).

Abstract classes and interfaces represent abstractions in C#. As a rule of thumb, you will generally use interfaces as your abstraction, unless you have some common default behavior you want all implementations to share; in which case, you should choose an abstract class.

The dependency inversion principle states that “high-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions” (Robert C. Martin: Agile Software Development, Principles, Patterns, and Practices, Pearson, 2002).

High-level modules tend to be consumers of services, and low-level modules tend to be the services themselves. So the first part of the DIP states that both the consumer and the service should depend on an abstraction, rather than the consumer depending on a specific service implementation. In the example, the abstraction will be an interface; the service will implement it, and the consumer will call it (figure 7.3).

CH07_F03_Brind

Figure 7.3 The PageModel and the SimpleCityService depend on an abstraction: the ICityService interface.

Now that the dependency chain is inverted, you need to design your ICityService interface. The second part of the DIP states that the interface should depend on abstractions too, not “details.” That is, the interface should not be tied to specific implementations. So your interface should not return implementation-specific types, like a DbDataReader, which only works with relational databases. It should depend on more general-purpose types like a List<T>. Your SimpleCityService class already does that, fortunately. So you will create an interface based on its existing API.

Add a new C# code file to the Services folder, and name it ICityService.cs. Note that if you are using Visual Studio, the Add... New Item dialog includes Interface as an option. Replace the existing code with the following.

Listing 7.3 The ICityService interface

using CityBreaks.Models;
 
namespace CityBreaks.Services
{
    public interface ICityService
    {
        Task<List<City>> GetAllAsync();
    }
}

Easy interface generation for Visual Studio Users

There is an even quicker way to generate an interface for Visual Studio users. Place your cursor on the SimpleCityService class name, and press Ctrl-. (period). From the dialog that appears, select Extract Interface... .

CH07_F04_Brind

The quick way to generate a new interface from an existing type in Visual Studio

In the next dialog, change the name of the type, and leave the single method selected. Leave the destination as a new file, and click OK.

CH07_F05_Brind

The Extract Interface dialog

Now you need to ensure that the low-level component depends on the abstraction. Change SimpleCityService, so it implements the following interface:

public class SimpleCityService : ICityService

Note that this step is not necessary if you used the Visual Studio wizard to extract the interface.

The final step is to get the high-level module, the CreateModel class, to also depend on the abstraction. How do you do that? Drum roll, please ... you use dependency injection.

7.1.4 Dependency injection

Dependency injection is a technique that helps us to achieve dependency inversion. As its name suggests, you inject the dependency into the consuming module, typically via its constructor as a parameter, and assign it to a private field for use within the consuming class. Injecting dependencies as arguments to a constructor method, as you will recall, helps us conform to the explicit dependencies principle.

The following code listing shows the CreateModel, altered to include an explicit constructor that takes an ICityService as a parameter. It assigns it to a private field, so it can be referenced within the class where needed.

Listing 7.4 Injecting ICityService dependency via CreateModel’s constructor

public class CreateModel : PageModel
{
    private readonly ICityService _cityService;    
    public CreateModel(ICityService cityService)   
    {
        _cityService = cityService;                
    }
    ...
}

Add a private field to the class to store the dependency.

Inject the dependency via the constructor.

Assign the injected dependency to the private field.

Now your dependency is explicit. The collaborating object (any type that implements the ICityService interface) needed by your CreateModel class is identified to the outside world by its presence in the class’s constructor.

7.2 Inversion of control containers

Of course, in C#, you cannot instantiate an interface, so how can this code make any sense? The actual type passed in to the constructor argument can be any type that implements the specified interface. At run time, an implementation of the interface is provided to the constructor. A built-in dependency injection container provides the implementation. This is more commonly referred to in Microsoft documentation as a service container, although more widely, you might also see this type of component referred to as an IoC container or a DI container.

So that leaves just one question: how does the container know which implementation to provide? And the answer is you tell it by registering your services with the container.

7.2.1 Service registration

Service registration takes place in the Program class by adding registrations to the WebApplicationBuilder’s Services property. You may recall seeing this in chapter 2 when I discussed using the IMiddleware interface for building middlewares, although I didn’t go into detail at the time. The standard web application template already includes a registration for Razor Pages through the AddRazorPages method, which is responsible for registering all the services Razor Pages depends on, including those that are responsible for generating and matching routes, handler method selection and page execution, as well as the Razor view engine itself.

The Services property is an IServiceCollection, which is the framework’s service container. It contains a collection of ServiceDescriptor objects, each one representing a registered service. A basic registered service consists of the service type, the implementation, and the service lifetime. The following listing shows how you can register the ICityService as a new ServiceDescriptor.

Listing 7.5 Registering the ICityService with the service container

builder.Services.AddRazorPages();
builder.Services.Add(new ServiceDescriptor(typeof(ICityService), typeof(SimpleCityService), ServiceLifetime.Transient));

However, you are more likely to use one of the lifetime-specific extension methods available on the IServiceCollection that takes the service type and implementation as generic parameters (figure 7.4).

builder.Services.AddTransient<ICityService, SimpleCityService>();

CH07_F06_Brind

Figure 7.4 Registration results in a ServiceDescriptor object being added to the IServiceCollection, consisting of a service type, implementation, and lifetime.

The service container’s job is to provide the correct implementation whenever a service type is requested—for example, being injected into a constructor. This process is also referred to as resolving the dependency. So when the container sees a request for an ICityService, it will provide an instance of SimpleCityService (figure 7.5).

CH07_F07_Brind

Figure 7.5 When the container sees a request for a service, it provides the implementation

7.2.2 Service lifetimes

The service container is not only responsible for resolving implementations. It is also responsible for managing the lifetime of the service. That is, the container is responsible for creating the service and destroying it, as determined by the lifetime the service is registered with. A service can be registered as having one of three lifetimes:

  • Singleton

  • Transient

  • Scoped

For each lifetime, there is an extension method that starts with the word Add, followed by the name of the lifetime. For example, you have used the AddTransient method to register the ICityService with the transient lifetime.

Singleton services

Services registered with the AddSingleton method are instantiated as singletons when the service is first requested and retained for the duration of the container’s lifetime, which is typically the same as the running application. As its name suggests, only one instance of a singleton can exist. It is reused for all requests. The vast majority of framework services—model binding, routing, logging, and so on—are registered as singletons. They all share the same characteristics in that they don’t have any state and are thread safe, which means the same instance can be used across multiple threads; this may be required to process concurrent requests. The same characteristics must also apply to any dependencies the singleton services rely on that are instantiated along with the service.

I will briefly digress from the main direction of our current application to explore how this works with a simple demonstration. You will create a service that exposes a value that is set in its constructor, and then register that service as a singleton. You will use a GUID for the value because it is almost certain to be different each time it is generated. Then you will render that value to the browser. You will note that the value doesn’t change when you refresh the page. Add a new C# class to the Services folder named LifetimeDemoService with the following code.

Listing 7.6 The LifetimeDemoService class

using System;
namespace CityBreaks.Services
{
    public class LifetimeDemoService
    {
        public LifetimeDemoService()
        {
            Value = Guid.NewGuid();
        }
        public Guid Value { get; }  
    }
}

The public Value property is set whenever the class constructor is called, which is when the container instantiates the service. You will register this service as a singleton, which should ensure it is only ever instantiated once during the life of the application:

builder.Services.AddRazorPages();
builder.Services.AddTransient<ICityService, SimpleCityService>();
builder.Services.AddSingleton<LifetimeDemoService>();

The service is registered with a version of the AddSingleton method that takes a single generic parameter representing the implementation. You don’t have an abstraction for this example. It’s not needed because this is a simple demonstration, and an abstraction will provide an unnecessary distraction from the main point of the examples that follow. Create a new Razor page called LifetimeDemo.cshtml in the Pages folder with the following code in the PageModel class file.

Listing 7.7 The LifetimeDemoModel code for demonstrating how service lifetimes work

using CityBreaks.Services;
using Microsoft.AspNetCore.Mvc.RazorPages;
 
namespace CityBreaks.Pages
{
    public class LifetimeDemoModel : PageModel
    {
        private readonly LifetimeDemoService _lifetimeDemoService;
        public LifetimeDemoModel(LifetimeDemoService lifetimeDemoService)
        {
            _lifetimeDemoService = lifetimeDemoService; 
        }
 
        public Guid Value { get; set; }
        public void OnGet()
        {
            Value = _lifetimeDemoService.Value;
        }
    }
}

Change the code in the Razor page itself by adding the highlighted lines from the following listing.

Listing 7.8 The LifetimeDemo Razor page

@page
@model CityBreaks.Pages.LifetimeDemoModel
@{
    ViewData["Title"] = "Lifetime demo";
}
<h2>Service Lifetime Demo</h2>
<p>The Singleton service returned @Model.Value</p>

Run the application, navigate to /lifetime-demo (remembering the effect of the KebabPageRouteParameterTransformer), and note the value rendered to the browser. Refresh the page, and confirm the value stays the same. Use a different browser to request the page. The value doesn’t change. This is because the value was set when the service was first instantiated, and as a singleton, the same instance of the service is shared by all consumers across all requests.

Transient services

Services registered with the AddTransient method are given a transient lifetime, meaning they are created each time they are resolved. These types of services should be lightweight, stateless services for which the cost of instantiation is relatively low. They are destroyed when the service scope is destroyed. In the context of an ASP.NET Core application, the scope is destroyed at the end of an HTTP request. If you have a complex dependency graph where the same service type is injected into multiple constructors, each consumer will receive its own instance of the service. The SimpleCityService is a good candidate for the transient lifetime, as it meets the definition of a service that maintains no state and has a low instantiation cost.

To see this working, you will inject a second instance of the service into the PageModel and render its value to the browser. Make the following changes to the LifetimeDemoModel class.

Listing 7.9 Injecting a second service to the PageModel

private readonly LifetimeDemoService _lifetimeDemoService;
private readonly LifetimeDemoService _secondService;                 
public LifetimeDemoModel(LifetimeDemoService lifetimeDemoService, 
    LifetimeDemoService secondService)                               
{
    _lifetimeDemoService = lifetimeDemoService; 
    _secondService = secondService;                                  
}
 
public Guid Value { get; set; }
public Guid SecondValue { get; set; }                                
public void OnGet()
{
    Value = _lifetimeDemoService.Value;
    SecondValue = _secondService.Value;                              
}

Add a private field for the second service.

Inject a second instance of the LifetimeDemoService.

Assign it to the private field.

Add another public property to the PageModel.

Set its value to the second service’s Value.

Next change the code in the Razor page that renders the service values as follows.

Listing 7.10 Rendering values from both services

<p>The first transient service returned @Model.Value</p>
<p>The second transient service returned @Model.SecondValue</p>

Finally, change the registration in Program.cs to use the transient lifetime:

builder.Services.AddTransient<LifetimeDemoService>();

Run the application, navigate to /lifetime-demo, and note that the values rendered to the browser are different. Every time you refresh the page, they change, confirming that each service is instantiated every time they are requested.

Scoped services

The final lifetime option is the scoped lifetime. As mentioned previously, in ASP.NET Core web applications, a scope is an HTTP request, which means scoped services are created once per HTTP request. What actually happens is that an instance of the container is created for each HTTP request and is destroyed at the end of the request. Scoped services are resolved by this scoped container and are consequently destroyed when the container is destroyed at the end of the scope. Scoped services differ from transient services in that only one instance of a scoped service is resolved per scope, whereas multiple instances of a transient service can be instantiated.

Once instantiated, the scoped service is reused as many times as needed during the scope (request) and disposed of at the end of the request. Each request gets its own scoped container, so concurrent requests to the same resource will work with different containers.

To see how this works, all you need to do is change the registration of the LifetimeDemoService to use the AddScoped method:

builder.Services.AddScoped<LifetimeDemoService>();

Then change the Razor page to reference the scoped service values:

<p>The first scoped service returned @Model.Value</p>
<p>The second scoped service returned @Model.SecondValue</p>

Now when you run the application, you should see that both services produce the same value. Once the LifetimeDemoService is instantiated, it is reused wherever it is needed within the scope of the HTTP request. It is similar, in that respect, to a singleton, scoped to a request, rather than the application’s lifetime.

Scoped lifetimes are most suitable for services that are expensive to instantiate and/or need to maintain state during a request. In a Razor Pages application, one of the most frequently used services that benefits from a scoped lifetime is the Entity Framework DbContext, which we will look at in more detail in the next chapter. The DbContext meets both criteria in that it is expensive to instantiate because it creates a connection to an external resource (the database), and it may need to maintain information about data that it has retrieved from the database.

7.2.3 Captive dependencies

When choosing a lifetime for your service, you should also take into account the lifetime of any dependency the service relies on. For example, if the dependency is a DbContext that has been registered with the scoped lifetime, you should ensure your service is also registered with a scoped lifetime. Otherwise, you could end up suffering from an issue known as captive dependencies. This issue arises when dependencies are registered with a shorter lifetime than their consumer. The DI container will raise an InvalidOperationException if you try to consume a scoped service from within a singleton, but you receive no such protection against consuming a transient service from within a singleton. If you inject a transient service into another service that is then registered as a singleton, the dependencies also effectively become singletons because the consuming service will only ever have its constructor called once during the lifetime of the application, and it will only ever be destroyed when the application stops.

Let’s have a look at this, so you understand it clearly. Add the following code as a new C# class to the Services folder.

Listing 7.11 The SingletonService class

namespace CityBreaks.Services
{
    public class SingletonService
    {
        private readonly LifetimeDemoService _dependency;
        public SingletonService(LifetimeDemoService dependency)
        {
           _dependency = dependency;
        }
        public Guid DependencyValue => _dependency.Value;
    }
}

The code is simple; the SingletonService class takes your existing LifetimeDemoService as a dependency and uses it to generate a value. Now you need to register the SingletonService as a singleton, while leaving the LifetimeDemoService registered with a transient lifetime:

builder.Services.AddTransient<LifetimeDemoService>();
builder.Services.AddSingleton<SingletonService>();

Change the markup in the Razor page to output the following:

<p>The singleton service's transient dependency returned @Model.Value</p>

Run the page, and refresh it. Notice that the value never changes. You don’t get a new instance of the LifetimeDemoService on each request because the consumer’s constructor is not being called, since it is a singleton.

7.2.4 Other service registration options

The aforementioned examples use one of the Add[LIFETIME] methods that takes two generic arguments—the first representing the service type and the second representing the implementation. This is the pattern you are likely to use most often. We have also looked at the version of the Add[LIFETIME] method that takes an implementation. Here we’ll review some other registration options that provide additional capabilities.

Imagine your SimpleCityService needs some constructor arguments passed to it. You can do this by passing in a factory that defines the arguments to be passed:

builder.Services.AddTransient<ICityService>(provider => new 
 SimpleCityService(args));

If the constructor arguments include a dependency from the container, the factory provides access to the service, so you can resolve the dependency. The following example shows how this works if the SimpleCityService takes a dependency on an implementation of IMyService as well as args. You use the IServiceProvider GetService method to resolve the dependency. We will look at other ways to access services directly from the service provider at the end of the chapter:

builder.Services.AddTransient(provider => 
    new SimpleCityService(args, provider.GetService<IMyService>())
);

The factory option is preferred because it hands responsibility for newing up, or activating, the service to the service container. And if the container is responsible for service activation, it also takes responsibility for service disposal. There is an alternative method that applies to singleton services, which involves passing in the constructed service:

builder.Services.AddSingleton<IMyService>(new MyService(args));

When you register a service using this approach, you must take responsibility for its disposal too. The same is true if you pass in the constructed service using the implementation-only option:

builder.Services.AddSingleton(new MyService(args));

7.2.5 Registering multiple implementations

It is possible to register multiple implementations of a service by repeating the relevant Add[LIFETIME] method with the same service type but a different implementation:

builder.Services.AddTransient<ICityService, SimpleCityService>();
builder.Services.AddTransient<ICityService, CityService>();

This raises an obvious question: which one will get resolved when its abstraction is injected into a constructor? The answer to that question is the last one you registered. So another question arises: how is the capability to inject multiple implementations useful?

Imagine you have several different implementations of a service, but you rely on runtime data to determine which implementation to use. For example, you might want to calculate prices, taxes, and discounts based on the visitor’s location. You could fill one service with conditional code for each location that you serve, but you can imagine that approach getting very messy very quickly, especially if the calculations are complex. And you can also imagine the maintenance issues if you need to update the code to reflect changes in laws in one territory, for example. That opens up the possibility of inadvertently changing code for other locations and introducing bugs unrelated to the change you needed to make.

Instead, you could provide a separate implementation for each location. Consider the following simple interface: IPriceService.

Listing 7.12 The IPriceService interface

public interface IPriceService
{
    string GetLocation();
    double CalculatePrice();
}

This interface defines two methods—one that returns the location that applies to any specific implementation and another that represents the logic for calculating prices. Let’s assume each implementation of this service definition returns the ISO 3166-1 Alpha-2 code you already know about, except for a default price service, which returns "XX". The US version is shown in listing 7.13. Others are available in the download that accompanies this section (http://mng.bz/o54p).

Listing 7.13 Example implementation for an IPriceService for the USA

public class UsPriceService : IPriceService
{
    public string GetLocation() => "us";
    public double CalculatePrice()
    {
        ...
    }
}

You register a variety of implementations with the service container:

builder.Services.AddScoped<IPriceService, FrPriceService>();
builder.Services.AddScoped<IPriceService, GbPriceService>();
builder.Services.AddScoped<IPriceService, UsPriceService>();
builder.Services.AddScoped<IPriceService, DefaultPriceService>();

If you were to inject IPriceService into a PageModel constructor, you would always get the DefaultPriceService, as established above, being that it is the last one registered. However, you can also inject an IEnumerable<IPriceService>, which resolves to a collection of all registered implementations. Then it is just a question of selecting the implementation that applies to the current request.

I’m a fan of Cloudflare (https://www.cloudflare.com/), which provides a range of web-related services, including geolocation (other geolocation service providers are available), whereby they identify the location of a request based on its IP address. The location is made available to application code in the request headers as an ISO-3166-1 Alpha-2 code, or "XX" where it is not possible to resolve the location. The following listing shows an example of how to use this header value to resolve the correct service to call based on the current request.

Listing 7.14 Resolving one from a number of registered services

public class CityModel : PageModel
{
    private readonly IEnumerable<IPriceService> _priceServices;
    public CityModel(IEnumerable<IPriceService> priceServices)     
    {
        _priceServices = priceServices;
    }
 
    public void OnGet()
    {
        var locationCode = Request.Headers["CF-IPCountry"];        
        var priceService = _priceServices.FirstOrDefault(s=> s.GetLocation()  
         == locationCode);                                       
        // do something with priceService
    }
}

Inject a collection representing all registered implementations.

Obtain the runtime data used to define the implementation applicable to this request.

Query the collection for a service that matches the predicate passed in to the FirstOrDefault method.

There are two clear benefits to adopting this pattern. The first is that each IPriceService implementation is location specific, which reduces the amount of code they require and results in a simpler maintenance experience. The second is that if you want to cater to additional locations, you just need to create a new service and register it along with the others. It will automatically be resolved as part of the injected collection.

There is another way to register services that will result in the first registration to be resolved in the situation where multiple implementations are registered, instead of the last. That is to use the TryAdd<LIFETIME> method. If you repeat the registration of the IPriceService implementations using TryAddScoped (as shown in the following listing), the first one will be resolved, unless you inject an IEnumerable.

Listing 7.15 TryAdd<LIFETIME> resulting in the first implementation being resolved

builder.Services.TryAddScoped<IPriceService, FrPriceService>();    
builder.Services.TryAddScoped<IPriceService, GbPriceService>();
builder.Services.TryAddScoped<IPriceService, UsPriceService>();
builder.Services.TryAddScoped<IPriceService, DefaultPriceService>();

The implementation that was registered first is resolved.

So when would you use the TryAdd method for registering services? Typically, you would use this approach if you wanted to ensure additional registrations made accidentally are not used by default. This could happen if it is not clear what registrations are being made because they are hidden within an extension method—for example, the AddRazorPages method. The library authors might want to ensure their registration is used regardless of what consumers of the framework subsequently try to do.

7.3 Other ways to access registered services

Constructor injection is likely to be the most common way you work with registered services. However, there are other ways to access services you should be aware of. You will likely use some of these options at some stage, but they have their caveats. The options include injecting directly into Razor files, method injection, and retrieving services directly from the service container.

7.3.1 View injection

Some services provided by the framework are designed to assist with the generation of HTML. One example is the IHtmlLocalizer service, which is used for localizing snippets of HTML in web applications that need to handle multiple languages. It serves no purpose outside of a Razor page or view. It is possible to inject this service into the PageModel of a page that needs it and then assign it to a public property, so it is accessible via the Model in the Razor page itself. But a better solution is to simply inject the service directly into the page using the @inject directive.

Listing 7.16 Using the @inject directive to inject services into a Razor page

@page
@inject IHtmlLocalizer<IndexModel> htmlLocalizer  
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}
  
<div class="text-center">
    <h1>Welcome</h1>
    <p>@htmlLocalizer["Intro"]</p>                
</div>

The IHtmlLocalizer<T> service is injected using the @inject directive and assigned to the variable htmlLocalizer.

The localizer service is used to localize a snippet of HTML identified as "Intro".

I should stress that this approach is fine when you are only using it for HTML-based services. You should not inject any services containing business logic directly into the page. We keep our business logic away from HTML, don’t we?

7.3.2 Method injection

Out of the box, the default services container only supports constructor injection. However, ASP.NET Core adds method parameter injection in a couple of places. You have already seen an example of this in chapter 2, when we looked at creating conventional middleware. If you recall, you injected an ILogger<T> into the InvokeAsync method:

public async Task InvokeAsync(HttpContext context, 
 ILogger<IpAddressMiddleware> logger)

But what about handler methods? After all, handler method parameters are seen as binding targets by the model binder. What happens when the model binder encounters an IPriceService parameter? Your application breaks. That’s what happens, unless you prefix the service parameter with the FromServices attribute:

public async Task OnGetAsync([FromServices]IPriceService service)
{
    // do something with service
}

This is a useful pattern for services that are expensive to create but are only used a fraction of the time in a Razor page. It might be, for example, that you have a named handler in a page that needs a service that is not required by the OnGet and OnPost handlers, and the named handler is only called under certain circumstances. In this scenario, it makes little sense to inject the service into the PageModel constructor. The FromServices attribute lets you scope the service purely to the handler method that needs it, and it will only be resolved when it is needed.

7.3.3 Directly from the service container with GetService and GetRequiredService

Sometimes, you will need to access the service container directly for a service. This approach is known as the service locator pattern. That sounds like it’s a good thing, being a design pattern and all, but it is generally considered an anti-pattern and should be avoided. However, sometimes you don’t have a choice. You have already seen an example of this when you used a factory to register a service that takes another service as a dependency earlier in the chapter.

Definition An anti-pattern is a commonly used solution to a recurring problem (a pattern), that is usually suboptimal in some way. This might be because the solution introduces new problems or because it simply moves the problem elsewhere.

The IServiceProvider service provides access to registered services. It has one method, GetService, which returns the specified service, or null if it is not found. In addition, there is an extension method, GetRequiredService, that throws an exception if the specified service is not found. The IServiceProvider is injected into the consumer, which then uses it to retrieve the services it needs.

Listing 7.17 Example usage of the service locator pattern

public class IndexModel : PageModel
{
    private readonly IServiceProvider _serviceProvider;           
 
    public IndexModel(IServiceProvider serviceProvider) =>] 
      _serviceProvider = serviceProvider;                       
 
    public List<City> Cities { get; set; }                        
    public async Task OnGetAsync()
    {
        var cityService = 
         _serviceProvider.GetRequiredService<ICityService>();   
        Cities = await cityService.GetAllAsync();
    }
}

Inject the IServiceProvider into the class constructor.

Recalling what I said about the explicit dependencies principle, you may be able to discern why the service locator is an anti-pattern. It is not clear from the code in listing 7.17 what the dependencies of the IndexModel are—apart from the service provider. In fact, it still depends on the ICityService, but that detail is no longer visible to code outside of the class.

The service provider is also available as a request feature (http://mng.bz/neE2), so you don’t even need to inject the provider into classes that have access to the HttpContext. You could replace the line of code that resolves the city service in listing 7.17 with the following:

var cityService = 
 HttpContext.RequestServices.GetRequiredService<ICityService>();

Dependency injection and the other terms that accompany it sound complicated, but the reality is that it is a pretty simple technique that helps to achieve high-quality code. The built-in service container should suffice for most use cases, but if you find yourself needing something more advanced, you can use one of the many (usually free and open source) third-party containers that support ASP.NET Core. Integration is usually quite straightforward and should be fully documented by the vendor.

In the next chapter, we will look at working with data in a Razor Pages application. While we do that, we will create a new service that obtains data from a database and seamlessly swap out the existing service for a new one, demonstrating one of the key advantages of using DI.

Summary

  • Dependency injection (DI) is a key feature in ASP.NET Core.

  • DI helps you implement inversion of control, a technique that promotes loose coupling of code.

  • Services are injected into classes that depend on them as explicit dependencies.

  • The dependency inversion principle (DIP) states that high-level classes and low-level classes should depend on abstractions, such as interfaces.

  • You configure services in Program.cs via the WebApplication Services property, which represents the configured services for the application.

  • Services are registered as a type and an implementation with a service container.

  • Services are registered using one of three lifetimes: singleton, transient, or scoped.

  • Only one instance of a singleton can exist. It lasts for the lifetime of the container.

  • Transient services are resolved every time they are requested.

  • Scoped services last for the duration of a web request in ASP.NET Core.

  • Multiple implementations of the same service can be registered. The last one registered will be resolved.

  • You can access all registered implementations by injection and IEnumerable <ServiceType>.

  • You can inject into page handler methods by prefixing the service parameter with [FromServices].

  • You can inject directly into Razor pages via the @inject attribute.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset