7

Deep Dive into Dependency Injection

In this chapter, we explore the ASP.NET Core Dependency Injection (DI) system and how to leverage it efficiently, along with its limits and its capabilities. We also cover how to compose objects using DI, the meaning of inversion of control, and how to use the built-in DI container. We cover the concepts behind DI, too, and we also revisit our first three GoF design patterns using DI. This chapter is crucial to your journey into modern application design.

The following topics will be covered in this chapter:

  • What is dependency injection?
  • Revisiting the Strategy pattern
  • Revisiting the Singleton pattern
  • Understanding the Service Locator pattern
  • Revisiting the Factory pattern

What is dependency injection?

DI is a way to apply the Inversion of Control (IoC) principle. We could regard IoC as a broader version of the dependency inversion principle (the D in SOLID).

The idea behind DI is to move the creation of dependencies from the objects themselves to the program’s entry point (the composition root). That way, we can delegate the management of dependencies to an IoC container (also known as a DI container), which does the heavy lifting.

For example, object A should not know about object B that it is using. Instead, A should use an interface, I, implemented by B, and B should be resolved and injected at runtime.

Let’s decompose this:

  • Object A should depend on interface I instead of concretion B.
  • Instance B, injected into A, should be resolved at runtime by the IoC container.
  • A should not be aware of the existence of B.
  • A should not control the lifetime of B.

To go all out LEGO®, we could see IoC as drawing a plan to build a castle: you draw it, make or buy the blocks, and then you press the start button and the blocks assemble themselves as per your plan. By following that logic, you could create a new 4x4 block with a unicorn painted on its side, update your plan, and then press the restart button to rebuild the castle with that new block inserted into it, replacing an old one without affecting the structural integrity of the castle. By respecting the 4x4 block contract, everything should be updatable without impacting the rest of the castle.

By following that idea, if we needed to manage every single LEGO® block one by one, it would become incredibly complex very quickly! Therefore, managing all dependencies by hand in a project would be super tedious and error-prone, even in the smallest program. To help us solve this issue, IoC containers come into play.

Note

A DI container or IoC container is the same thing—they’re just different words that people use. I use both interchangeably in real life, but I’ll do my best to stick with IoC container in this book.

I chose the term IoC container because it seems more accurate than “DI container.” IoC is the concept (the principle), while DI is a way of inverting the flow of control (applying IoC). For example, you apply the IoC principle (inverting the flow) by injecting dependencies at runtime (doing DI) using a container.

The role of an IoC container is to manage objects for you. You configure them and then, when you ask for some abstraction, the associated implementation is resolved by the container. Moreover, the container manages the lifetime of dependencies, leaving your classes to do only one thing, the job you designed them for, without thinking about their dependencies, their implementation, or their lifetime!

The bottom line is that an IoC container is a DI framework that does the autowiring for you. We could regard DI as follows:

  1. The consumer of a dependency states its needs about one or more dependencies.
  2. The IoC container injects that dependency (implementation) upon creating the consumer, fulfilling its needs at runtime.

Next, we explore different DI areas: where to configure the container, available options, and a common object-oriented technique that is now a code smell.

The composition root

One of the first concepts behind DI is the composition root. The composition root is where you tell the container about your dependencies: where you compose your dependency trees. The composition root should be as close to the starting point of the program as possible.

From ASP.NET Core 6 onward, the composition root is in the Program.cs file, while previously it was in either Program.cs or Startup.cs, or both.

Note

As a LEGO® analogy, the composition root could be the paper sheet on which you draw your plan.

The starting point of an ASP.NET Core application is the Program class, which is now autogenerated by default. We must first create a WebApplicationBuilder (highlighted), and then we can use its Services property to register our dependencies, like this:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<Dependency1>();
builder.Services.AddSingleton<Dependency2>();
builder.Services.AddSingleton<Dependency3>();
builder.Services.AddDemoFeature();

Once that is done, we must create WebApplication itself (highlighted) to configure the ASP.NET Core middleware pipeline. You can write any code before the Run method call that starts the application, but this place is usually reserved for configuring the pipeline:

var app = builder.Build();
app.MapGet("/", () => "Hello World!");
// You can write any code here
app.Run();

It is imperative to remember that your program’s composition should be done in the composition root. That removes the need for all of those pesky new keywords spread around your code base and all the responsibilities that come with them. It centralizes the application’s composition into that location (that is, creating the plan to assemble the LEGO® blocks).

Registering your features elegantly

As we just saw, you should register dependencies in the composition root, but you can still organize your registration code. For example, you could split your application’s composition into multiple methods or classes, and then call them from your composition root. You could also use an auto-discovery mechanism to automate the registration of some services; we use packages that do that in subsequent chapters.

Note

The critical part remains centralizing the program composition.

As an example, most features of ASP.NET Core and other popular libraries provide one or more Add[Feature name]() extension methods to manage the registration of their dependencies, allowing you to register a “bundle of dependencies” with one method call. That’s very useful for organizing program composition into smaller, more cohesive units, such as by feature.

Side Note

A feature is the correct size as long as it stays cohesive. If your feature becomes too big, does too many things, or starts to share dependencies with other features, it may be the time for a redesign before losing control over it. That’s usually a good indicator of undesired coupling.

Using extension methods makes it reasonably easy to build such a bundle of dependencies to register a feature. As a rule of thumb, you should do the following:

  1. Create a static class named [subject]Extensions.
  2. As per the Microsoft recommendation, create that class in the Microsoft.Extensions.DependencyInjection namespace (the same as IServiceCollection).
  3. From there, create your IServiceCollection extension methods. Unless you need to return something else like a builder interface (see below), make sure to return the extended IServiceCollection; this allows method calls to be chained.

Note

Builder interfaces are used to configure more complex features, like ASP.NET Core MVC. For example, the AddControllers extension method returns an IMvcBuilder interface that contains a PartManager property. Moreover, there are extension methods that target the IMvcBuilder interface, allowing further configuration of the feature by requiring its registration first; that is, you can’t configure IMvcBuilder before calling AddControllers. You can also design your features to leverage that pattern when needed.

For example, if my feature were named Demo Feature, I’d write the following extension method:

using CompositionRoot.DemoFeature;
namespace Microsoft.Extensions.DependencyInjection
{
    public static class DemoFeatureExtensions
    {
        public static IServiceCollection AddDemoFeature(this IServiceCollection 
services)
        {
            return services
                .AddSingleton<MyFeature>()
                .AddSingleton<IMyFeatureDependency, MyFeatureDependency>()
            ;
        }
    }
}

Then, to use it, we could enter the following in the composition root:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDemoFeature();
}

If you are not familiar with extension methods, they come in handy for extending existing classes, like what we just did. For example, you could build a sophisticated library and a set of easy-to-use extension methods that allow consumers to learn and use your library easily while keeping advanced options and customization opportunities to a maximum; think ASP.NET Core MVC or System.Linq.

Object lifetime

I’ve talked about this a few times already: no more new; that time is over! From now on, the IoC container should do most of the jobs related to instantiating and managing objects for us.

However, before trying this out, we need to cover one last thing: object lifetime. When you create instances manually, using the new keyword, you create a hold on that object; you know when you create them and when you destroy them. That leaves no chance to control these objects from the outside, enhance them, intercept them, or swap them for another implementation. This is known as the Control Freak anti-pattern or code smell, explained in the Code smell – Control Freak section.

When using DI, you need to forget about controlling objects and start to think about using dependencies — more explicitly, using their interfaces. In ASP.NET Core, there are three possible lifetimes to choose from:

Lifetime

Description

Code sample

Transient

The container creates a new object every time you ask for one.

services.AddTransient<ISomeService, SomeService>();

Scoped

The container creates an object per HTTP request and passes that object around to all other objects that want to use it.

services.AddScoped<ISomeService, SomeService>();

Singleton

The container creates a single instance of that dependency and always passes that unique object around.

services.AddSingleton<ISomeService, SomeService>();

From now on, we manage most of our objects using one of those three scopes. Here are some questions to help you choose:

  • Do I need a single instance of my dependency? Yes? Use the singleton lifetime.
  • Do I need a single instance of my dependency shared over an HTTP request? Yes? Use the scoped lifetime.
  • Do I need a new instance of my dependency every time? Yes? Use the transient lifetime.

If you need a more complex lifetime, you may need to swap the built-in container to a third-party one (see the Using external IoC container section) or create your dependency tree manually in the composition root.

Note

A more general approach to object lifetime is to design the components to be singletons. When we can’t, then go for scoped. When scoped is also impossible, go for transient. This way, we maximize instance reuse, lower the overhead of creating objects, lower the memory cost of keeping those objects in memory, and lower the amount of garbage collection needed to remove unused instances.

For example, we can pick singleton blindly for stateless objects.

For stateful objects, where multiple consumers use the same instance of an object having a lifetime of singleton or scoped, we must ensure that the object is thread-safe since multiple consumers could try to access it simultaneously.

For stateful objects, an important reason is around the data contained in that object (its state). Say we load data that relates to the current user. In this case, we must make sure that data does not get leaked to other users. To do so, we can define the lifetime of that object to scoped when we want to allow reusing that state between multiple consumers. If that’s not the case, we can choose a transient lifetime to ensure every consumer gets their own instance. When that state object is reused often during a single request, a scoped lifetime should improve the performance.

Another point around long-lived objects is that they are inspected only once in a while by the garbage collector, while short-lived ones are often scanned and disposed of.

There are multiple variants of the three preceding examples, but the lifetimes remain. We use the built-in container throughout the book with many of its registration methods, so you should grow familiar with it by the end. The system offers good discoverability, so you could explore the possibilities by using IntelliSense or by reading the documentation.

Code smell – Control Freak

We’ve already stated that using the new keyword is a code smell or even an anti-pattern. However, do not ban the new keyword just yet. Instead, every time you use it, ask yourself whether the object you instantiated using the new keyword is a dependency that could be managed by the container and injected instead.

To help out with that, I borrowed two terms from Mark Seemann’s book Dependency Injection in .NET; the name Control Freak also comes from that book. He describes the following two categories of dependencies:

  • Stable dependencies
  • Volatile dependencies

Stable dependencies are dependencies that should not break your application when a new version of it is released. They should use deterministic algorithms (input X should always produce output Y; a.k.a. respecting the Liskov Substitution Principle (LSP), and you should not expect to change them with something else in the future. I’d say that most data structures could fall into this category: Data Transfer Objects (DTOs), List<T>, and so on. You can still instantiate objects using the new keyword when they fall into this category; it is acceptable since they are not likely to break anything or to change. But be careful because foreseeing whether a dependency is likely to change or not is very hard, even impossible, as we can’t know for sure what the future has to offer. For example, elements that are part of .NET could be considered stable dependencies.

Volatile dependencies are dependencies that can change, behaviors that could be swapped, or elements you may want to extend, basically, most of the classes you create for your programs such as data access and business logic classes. These are the dependencies that you should no longer instantiate using the new keyword. The primary way to break the tight coupling between implementations is to rely on interfaces instead.

To conclude this interlude: don’t be a control freak anymore, those days are behind you!

Tip

When in doubt, inject the dependency instead of using the new keyword.

Next, we briefly explore an ASP.NET Core extension point before revisiting three design patterns, but this time, by exploiting DI.

Using external IoC containers

ASP.NET Core provides an extensible built-in IoC container out of the box. It is not the most powerful IoC container because it lacks some advanced features, but it can do the job for most applications.

Rest assured, if it does not, you can change it for another. If you are used to another IoC container and want to stick to it or require some missing advanced features, you might want to do that.

As of today, Microsoft recommends using the built-in container first. If you don’t know ahead of time all the DI features that you will need, I’d go with the following strategy:

  1. Use the built-in container.
  2. When something cannot be done with it, look at your design and see if you can redesign your feature to work with the built-in container. This could help simplify your design and, at the same time, help maintain your software in the long run.
  3. If it is impossible to achieve your goal, then swap it for another IoC container.

Assuming the container supports it, it is super simple to swap. The third-party container must implement the IServiceProviderFactory<TContainerBuilder> interface. Then, in the Program.cs file, we must register that factory using the UseServiceProviderFactory<TContainerBuilder> method, like this:

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory<ContainerBuilder>(new ContainerBuilderFactory());

In this case, the ContainerBuilder and ContainerBuilderFactory classes are just wrappers around ASP.NET Core, but your third-party container of choice should provide you with those types. I suggest you visit their documentation to know more.

Once that factory is registered, we can configure the container using the ConfigureContainer<TContainerBuilder> method as usual, like this:

builder.Host.ConfigureContainer<ContainerBuilder>((context, builder) =>
{
    builder.Services.AddSingleton<Dependency1>();
    builder.Services.AddSingleton<Dependency2>();
    builder.Services.AddSingleton<Dependency3>();
});

That’s the only difference; the rest of the Program.cs file is as usual.

As I sense that you don’t feel like implementing your own IoC container just yet (or even ever), don’t worry; multiple third-party integrations already exist. Here is a non-exhaustive list:

  • Autofac
  • DryIoc
  • Grace
  • LightInject
  • Lamar
  • Stashbox
  • Unity

Some libraries extend the default container and add functionalities to it, which is an option that we explore in Chapter 9, Structural Patterns.

Next, we revisit the Strategy pattern, which will become the primary tool to compose our applications and add flexibility to our systems.

Revisiting the Strategy pattern

In this section, we leverage the Strategy pattern to compose complex object trees and use DI to dynamically create those instances without using the new keyword, moving away from being control freaks and toward writing DI-ready code.

The Strategy pattern is a behavioral design pattern that we can use to compose object trees at runtime, allowing extra flexibility and control over objects’ behavior. Composing our objects using the Strategy pattern should make our classes easier to test and maintain and put us on a SOLID path.

From now on, we want to compose objects and lower the amount of inheritance to a minimum. We call that principle composition over inheritance. The goal is to inject dependencies (composition) into the current class instead of depending on base class features (inheritance). Moreover, that allows behaviors to be extracted in external classes (SRP/ISP) and then reused in one or more other classes (composition) through their interface (DIP).

The following list covers the most popular ways of injecting dependencies into objects:

  • Constructor injection
  • Property injection
  • Method injection

We can also ask the container directly to resolve a dependency, which is known as the Service Locator (anti-)pattern. We explore the Service Locator pattern later in this chapter.

Let’s look at some theory and then jump into the code to see DI in action.

Constructor injection

Constructor injection consists of injecting dependencies into the constructor as parameters. This is the most popular and preferred technique by far. Constructor injection is useful for injecting required dependencies; you can add null checks to ensure that, also known as the guard clause (see the Adding a guard clause section).

Property injection

The built-in IoC container does not support property injection out of the box. The concept is to inject optional dependencies into properties. Most of the time, you want to avoid doing this because property injection leads to optional dependencies, leading to nullable properties, more null checks, and often avoidable code complexity. So when we think about it, it is good that ASP.NET Core left this one out of the built-in container.

You can usually remove the property injection requirements by reworking your design, leading to a better design. If you cannot avoid using property injection, you must use a third-party container or find a way to build the dependency yourself (maybe leveraging a factory).

Nevertheless, from a high-level view, the container would do something like this:

  1. Create a new instance of the class and inject all required dependencies into the constructor.
  2. Find extension points by scanning properties (this could be attributes, contextual bindings, or something else).
  3. For each extension point, inject (set) a dependency, leaving unconfigured properties untouched, hence its definition of an optional dependency.

There are a couple of exceptions to the previous statement regarding the lack of support:

  • Razor components (Blazor) support property injection through the use of the [Inject] attribute.
  • Razor contains the @inject directive, which generates a property to hold a dependency (ASP.NET Core manages to inject it).

We can’t call that property injection per se because they are not optional but required, and the @inject directive is more about generating code than doing DI. They are more about an internal workaround than “real” property injection. That said, that is as close as .NET can get to property injection.

Tip

I recommend aiming for constructor injection instead. Not having property injection should not cause you any problems.

Method injection

ASP.NET Core supports method injection only at a few locations, such as in a controller’s actions (methods), the Startup class (if you are using the pre-.NET 6 hosting model), and the middlewares’ Invoke or InvokeAsync methods. You are not able to liberally use method injection in your classes without some work on your part.

Method injection is also used to inject optional dependencies into classes. We can also validate those at runtime using null checks or any other required logic.

Tip

I recommend aiming for constructor injection whenever you can. Only rely on method injection when it is the only way or if it adds something. For example, in a controller, injecting a transient service in the only action that needs it instead of the constructor could save a lot of useless object instantiation and, by doing so, increase performance (less instantiation and less garbage collection).

Project – Strategy

In the Strategy project, we use the Strategy pattern and constructor injection to add (compose) a IHomeService dependency to the HomeController class.

The goal is to inject a dependency of the IHomeService type into the HomeController class. Then, send a view model to the view to render the page.

The service goes like this:

namespace Strategy.Services
{
    public interface IHomeService
    {
        IEnumerable<string> GetHomePageData();
    }
    public class HomeService : IHomeService
    {
        public IEnumerable<string> GetHomePageData()
        {
            yield return "Lorem";
            yield return "ipsum";
            yield return "dolor";
            yield return "sit";
            yield return "amet";
        }
    }
}

The IHomeService interface is the dependency that we want the HomeController class to have. The HomeService class is the implementation that we want to inject when instantiating HomeController, thereby inverting the flow of dependency.

To do that, we inject IHomeService into the controller using constructor injection, leading to the following steps:

  1. Create a private IHomeService field in the HomeController class.
  2. Create a HomeController constructor with a parameter of the IHomeService type.
  3. Assign the argument to the field.

In code, it looks like this:

using Strategy.Services;
namespace Strategy.Controllers;
public class HomeController : Controller
{
    private readonly IHomeService _homeService;
    public HomeController(IHomeService homeService)
    {
        _homeService = homeService;
    }
    // Omitted action methods 
}

The use of private readonly fields is beneficial for two reasons:

  • They are private, so you do not expose your dependencies outside of the class (encapsulation).
  • They are readonly, so you can only set the value during the initialization; usually only once. In the case of constructor injection, this ensures that the injected dependency, referenced by the private field, cannot be changed by other parts of the class.

If we run the application now, we get the following error:

InvalidOperationException: Unable to resolve service for type
'Strategy.Services.IHomeService' while attempting to activate
'Strategy.Controllers.HomeController'.

This error tells us that we forgot about something essential: to tell the container about the dependency.

To do that, we need to map the injection of IHomeService to an instance of HomeService. Due to our class’s nature, we are safe to use the singleton lifetime (one single instance). Using the extension methods provided, in the composition root, we only need to add the following line:

builder.Services.AddSingleton<IHomeService, HomeService>();

Now, if we rerun the app, the home page should load. That tells ASP.NET to inject a HomeService instance when a class depends on the IHomeService interface.

We have just completed our first implementation of constructor injection using ASP.NET Core—it’s as easy as that.

To recap constructor injection, we need to do the following:

  1. Create a dependency and its interface.
  2. Inject that dependency into another class through its constructor.
  3. Create a binding that tells the container how to handle the dependency.

    Note

    We can also inject classes directly, but until you feel that you’ve mastered the SOLID principles, I’d recommend sticking with injecting interfaces.

Next, we practice the use of view models.

Adding the View Model

Now that we’ve injected the service that contains the data to display in the HomeController class, we need to display it. To achieve that, we decided to use the View Model pattern. The view model’s goal is to create a view-centric model and then use it to render that view.

Here is what we need to do:

  1. Create a View Model class (HomePageViewModel).
  2. Update the Home/Index view to use the view model and display the information that it contains.
  3. Create and send an instance of HomePageViewModel to the view from the controller.

The HomePageViewModel class is exposing the SomeData property publicly and expects that data to be injected when instantiated. The code looks like this:

namespace Strategy.Models;
public class HomePageViewModel
{
    public IEnumerable<string> SomeData { get; }
    public HomePageViewModel(IEnumerable<string> someData)
    {
        SomeData = someData ?? throw new ArgumentNullException(nameof(someData));
    }
}

That’s another example of constructor injection.

Then, after a few updates (highlighted), the Views/Home/Index.cshtml view looks like this:

@model HomePageViewModel
@{
    ViewData["Title"] = "Home Page";
}
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    @if(Model != null)
    {
        <p>Here are your data:</p>
        <ul class="list-group">
        @foreach (var item in Model.SomeData)
        {
            <li class="list-group-item">@item</li>
        }
        </ul>
    }
</div>

Now we need to pass an instance of HomePageViewModel to the view. We are doing that in the Index action, like this:

public IActionResult Index()
{
    var data = _homeService.GetHomePageData();
    var viewModel = new HomePageViewModel(data);
    return View(viewModel);
}

In that code, we used the _homeService field to retrieve the data through the IHomeService interface. It is important to note that at this point, the controller is not aware of the implementation; it depends only on the contract (the interface). Then we create the HomePageViewModel class using that data. Finally, we dispatch the instance of HomePageViewModel to the view for rendering.

Note

As you may have noticed, I used the new keyword here. In this case, I find that instantiating a view model inside the controller’s action is acceptable. However, we could have used method injection or any other technique to help with object creation, such as a factory.

Next, we leverage a guard clause to make homeService mandatory.

Adding a guard clause

We’ve already stated that constructor injection is reliable and is used to inject the required dependencies. However, one thing bothers me from the last code sample: nothing guarantees us that _homeService is not null.

We could check for nulls in the Index method, like this:

public IActionResult Index()
{
    var data = _homeService?.GetHomePageData();
    var viewModel = new HomePageViewModel(data);
    return View(viewModel);
}

But as the controller grows, we may write null checks for that dependency many times in multiple locations. Then we should do the same null check in the view. Otherwise, we would loop a null value, which could cause a runtime error.

To avoid that duplication of logic and the number of possible errors that could come with it at the same time, we can add a guard clause.

A guard clause does as its name implies: it guards against invalid values. Most of the time, it guards against null. When you pass a null dependency into an object, the guard clause testing that parameter should throw an ArgumentNullException.

By using a throw expression, from C# 7 (See Appendix A for more information), we can simply write this:

public HomeController(IHomeService homeService)
{
    _homeService = homeService ?? throw new ArgumentNullException(nameof(homeService));
}

This throws an ArgumentNullException when homeService is null; otherwise, it assigns the homeService parameter value to the _homeService field. Of course, with the introduction of the nullable reference types (see Appendix A), the possibility of receiving a null argument is less likely, but it is still possible that it happens at runtime.

Important Note

A built-in container will automatically throw an exception if it can’t fulfill all dependencies during the instantiation of a class (such as HomeController). That said, it does not mean that all third-party containers act the same. Moreover, that does not protect you from passing null to a manually instantiated instance (even if we should use DI, it does not mean it won’t happen). As a matter of preference, I like to add them no matter what, but they are not required.

We can apply a guard clause to the HomePageViewModel class as well, but it would be redundant to go over that same process a second time.

We now have everything that we need to render the home page. More importantly, we achieved that without directly coupling the HomeController class with HomeService. Instead, we depend only on the IHomeService interface—a contract. By centralizing composition into the composition root, we could change the resulting home page by swapping the IHomeService implementation in the Program.cs file without impacting the controller or the view.

Conclusion

In this section, we saw that the strategy pattern went from a simple behavioral GoF pattern to the cornerstone of DI. We explored different ways of injecting dependencies with a strong focus on constructor injection.

Constructor injection is the most commonly used approach as it injects required dependencies, which are the ones we most often need. Method injection allows injecting algorithms, shared states, or contexts in a method that could not otherwise access that information. We can use property injection to inject optional dependencies, which should happen as rarely as possible.

You can see optional dependencies as code smells because if the class has an optional role to play, it also has a primary role leading to two responsibilities. Moreover, it could be better to move that optional role to another class if it’s optional or redesign that part of the system.

To practice what you just learned, I invite you to create a class that implements IHomeService and changes the mapping in the Program.cs file from HomeService to your new class and see how easy it is to change the home page’s list. To go even further, you could connect your implementation to a database, Azure Table, Redis, a JSON file, or any other data source that you can think of.

Next, we revisit a design pattern that is now an anti-pattern while exploring the singleton lifetime replacing it.

Revisiting the Singleton pattern

The Singleton pattern is obsolete, goes against the SOLID principles, and we replace it with a lifetime, as we’ve already seen. This section explores that lifetime and recreates the good old application state, which turns out to be nothing more than a singleton scoped dictionary.

We explore two examples here: one about the application state, in case you were wondering where that feature disappeared to. Then, the Wishlist project also uses the singleton lifetime to provide an application-level feature. There are also a few unit tests to play with the testability and to allow safe refactoring.

Project – Application state

You might remember the application state if you programmed ASP.NET using .NET Framework or the “good” old classic ASP with VBScript. If you don’t, the application state was a key/value dictionary that allowed you to store data globally in your application, shared between all sessions and requests. That is one of the things that ASP always had and other languages, such as PHP, did not (or does not easily allow).

For example, I remember designing a generic reusable typed shopping cart system with classic ASP/VBScript. VBScript was not a strongly typed language and had limited object-oriented capabilities. The shopping cart fields and types were defined at the application level (once per application), and then each user had their own “instance” containing the products in their “private shopping cart” (created once per session).

In ASP.NET Core, there is no more Application dictionary. To achieve the same goal, you could use a static class or static members, which is not the best approach; remember that global objects (static) make your application harder to test and less flexible. We could also use the Singleton pattern or create an ambient context, which would allow us to create an application-level instance of an object. We could even mix that with a factory to create end user shopping carts, but we won’t; these are not the best solution either. Another way could be to use one of the ASP.NET Core caching mechanisms, memory cache, or distributed cache, but this is a stretch.

We could also save everything in a database to persist the shopping cart between visits, but that is not related to the application state and requires a user account, so we will not do that either.

We could save the shopping cart on the client-side using cookies, local storage, or any other modern mechanism to save data on the user’s computer. However, we’d get even further from the application state than using a database.

For most cases requiring an application state-like feature, the best approach would be to create a standard class and an interface and then register the binding with a singleton lifetime in the container. Finally, you inject it into the component that needs it, using constructor injection. Doing so allows the mocking of dependencies and changing the implementations without touching the code but the composition root.

Tip

Sometimes, the best solution is not the technically complex one or design pattern-oriented; the best solution is often the simplest. Less code means less maintenance and fewer tests, resulting in a simpler application.

Let’s implement a small program that simulates the application state. The API is a single interface with two implementations. The program also exposes part of the API over HTTP, allowing users to get or set a value associated with the specified key. We use the singleton lifetime to make sure the data is shared between all requests.

The interface looks like the following:

public interface IApplicationState
{
    TItem? Get<TItem>(string key);
    bool Has<TItem>(string key);
    void Set<TItem>(string key, TItem value) where TItem : notnull;
} 

We can get the value associated with a key, associate a value with a key (set), and validate whether a key exists.

The Program.cs file contains the code responsible for handling HTTP requests. It is not using MVC, but minimal APIs. The two implementations can be swapped by commenting or uncommenting the first line of the Program.cs file, which is #define USE_MEMORY_CACHE. That changes the dependency registration, as highlighted in the following code:

var builder = WebApplication.CreateBuilder(args);
#if USE_MEMORY_CACHE
        builder.Services.AddMemoryCache();
        builder.Services.AddSingleton<IApplicationState, ApplicationMemoryCache>();
#else
        builder.Services.AddSingleton<IApplicationState,
ApplicationDictionary>();
#endif
var app = builder.Build();
app.MapGet("/", (IApplicationState myAppState, string key) =>
{
    var value = myAppState.Get<string>(key);
    return $"{key} = {value ?? "null"}";
});
app.MapPost("/", (IApplicationState myAppState, SetAppState dto) =>
{
    myAppState.Set(dto.Key, dto.Value);
    return $"{dto.Key} = {dto.Value}";
});
app.Run();
public record class SetAppState(string Key, string Value);

The first implementation uses the memory cache system, and I thought it would be educational to show that to you. Caching data in memory is something you might need to do sooner rather than later. Second, we are hiding the cache system behind our implementation, which is also educational. Finally, we needed two implementations, and using the cache system was a pretty straightforward implementation.

Here is the ApplicationMemoryCache class:

public class ApplicationMemoryCache : IApplicationState
{
    private readonly IMemoryCache _memoryCache;
    public ApplicationMemoryCache(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
    }
    public TItem Get<TItem>(string key)
    {
        return _memoryCache.Get<TItem>(key);
    }
    public bool Has<TItem>(string key)
    {
        return _memoryCache.TryGetValue<TItem>(key, out _);
    }
    public void Set<TItem>(string key, TItem value)
    {
        _memoryCache.Set(key, value);
    }
}

Note

The ApplicationMemoryCache class is a thin layer over IMemoryCache, hiding the implementation details. That type of class is called a façade. We talk more about the Façade design pattern in Chapter 9, Structural Patterns.

The second implementation uses ConcurrentDictionary<string, object> to store the application state data and ensure thread safety as multiple users could simultaneously use the application state. The ApplicationDictionary class is almost as simple as ApplicationMemoryCache:

public class ApplicationDictionary : IApplicationState
{
    private readonly ConcurrentDictionary<string, object> _memoryCache = new();
    public TItem? Get<TItem>(string key)
    {
        if (!Has<TItem>(key))
        {
            return default;
        }
        return (TItem)_memoryCache[key];
    }
    public bool Has<TItem>(string key)
    {
        return _memoryCache.ContainsKey(key) && _memoryCache[key] is TItem;
    }
    public void Set<TItem>(string key, TItem value)
        where TItem : notnull
    {
        _memoryCache[key] = value;
    }
}

We can now use any of the two implementations without impacting the rest of the program. That demonstrates the strength of DI when it comes to dependency management. Moreover, we control the lifetime of the dependencies from the composition root.

If we were to use the IApplicationState interface in another class, say SomeConsumer, its usage could look similar to the following:

namespace ApplicationState;
public class SomeConsumer
{
    private readonly IApplicationState _myApplicationWideService;
    public SomeConsumer(IapplicationState myApplicationWideService)
    {
        _myApplicationWideService = myApplicationWideService ?? throw new ArgumentNullException(nameof(myApplicationWideService));
    }
    public void Execute()
    {
        if (_myApplicationWideService.Has<string>("some-key"))
        {
            var someValue = _myApplicationWideService.Get<string>("some-key");
            // Do something with someValue
        }
        // Do something else like:
        _myApplicationWideService.Set("some-key", "some-value");
        // More logic here
    }
}

In that code, SomeConsumer depends only on the IApplicationState interface, not on IMemoryCache or Dictionary<string, object>. Using DI allows us to hide the implementation by inverting the control of dependencies. It also breaks direct coupling between concrete implementations, programming against interfaces like the DIP prescribes.

Here is a diagram illustrating our application state system, making it visually easier to notice how it breaks coupling:

Figure 7.1 – DI-oriented diagram representing the application state system

Figure 7.1: DI-oriented diagram representing the application state system

From this sample, let’s remember that the singleton lifetime allows us to reuse objects between requests and share them application-wide. Moreover, hiding implementation details behind interfaces can improve the flexibility of our design.

Project – Wishlist

Let’s get into another sample to illustrate the use of the singleton lifetime and DI. Seeing DI in action should help with understanding it, and then leveraging it to create SOLID software.

Context: The application is a site-wide wishlist where users can add items. Items should expire every 30 seconds. When a user adds an existing item, the system should increment the count and reset the item’s expiration time. That way, popular items stay on the list longer, making it to the top. The system should sort the items by count (highest first) when displayed.

Note

30 seconds is very fast, but I’m sure that you don’t want to wait for days before an item expires when running the app.

The program is a tiny web API that exposes two endpoints:

  • Add an item to the wishlist (POST).
  • Read the wishlist (GET).

The wishlist interface looks like this:

public interface IWishList
{
    Task<WishListItem> AddOrRefreshAsync(string itemName);
    Task<IEnumerable<WishListItem>> AllAsync();
}
public record class WishListItem(string Name, int Count, DateTimeOffset Expiration);

The two operations are there, and by making them async (returning a Task<T>), we could implement another version that relies on a remote system, such as a database, instead of an in-memory store. Then, the WishListItem record class is part of the IWishList contract; it is the model. To keep it simple, the wishlist only stores the names of items.

Note

Trying to foresee the future is not usually a good idea, but designing APIs to be awaitable is generally a safe bet. Other than this, I’d recommend you stick to your specifications and use cases. When you try to solve problems that do not exist yet, you usually end up coding a lot of useless stuff, leading to additional unnecessary maintenance and testing time.

In the composition root, we must set the IWishList implementation instance to a singleton scope, so all users share the same instance. Meanwhile, let’s look at the minimal APIs that handle the HTTP requests, highlighted below. To make it easier for you, here is the whole Program.cs file:

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .ConfigureOptions<InMemoryWishListOptions>()
    .AddTransient<IValidateOptions<InMemoryWishListOptions>, InMemoryWishListOptions>()
    .AddSingleton(serviceProvider => serviceProvider.GetRequiredService<IOptions<InMemoryWishListOptions>>().Value)
    // The singleton registration
    .AddSingleton<IWishList, InMemoryWishList>() 
;

Note

If you are wondering where IConfigureOptions, IValidateOptions, and IOptions come from, we are covering the ASP.NET Core Options pattern in the next chapter.

var app = builder.Build();
app.MapGet("/", async (IWishList wishList) => await wishList.AllAsync());
app.MapPost("/", async (IWishList wishList, CreateItem? newItem) =>
{
    if (newItem?.Name == null)
    {
        return Results.BadRequest();
    }
    var item = await wishList.AddOrRefreshAsync(newItem.Name);
    return Results.Created("/", item);
});
app.Run();
public record class CreateItem(string? Name);

The GET endpoint delegates the logic to the injected IWishList implementation and returns the result, while the POST endpoint also validates the CreateItem DTO.

Note

As of .NET 6, we can’t use DataAnnotations to validate minimal APIs. At the end of the chapter, I left a few links that explain the differences between minimal APIs and Controllers/MVC. Moreover, we explore FluentValidation in Chapter 15, Getting Started with Vertical Slice Architecture, that we can leverage for more complex validation. In this case, it was fairly simple to validate the input manually using a single if statement.

To help us implement the InMemoryWishList class, we started by writing some tests to back our specifications up. Since static members are hard to configure in tests (remember globals?), we borrowed a concept from the ASP.NET Core memory cache and created an ISystemClock interface that abstracts away the static call to DateTimeOffset.UtcNow. This way, we can program the value of UtcNow in our tests to create expired items. Here’s the clock interface and implementation:

namespace Wishlist.Internal;
public interface ISystemClock
{
    DateTimeOffset UtcNow { get; }
}
public class SystemClock : ISystemClock
{
    public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}

The unit tests file would be many pages long, so here is the outline:

namespace Wishlist;
public class InMemoryWishListTest
{
    // Constructor and private fields omitted
    public class AddOrRefreshAsync : InMemoryWishListTest
    {
        [Fact]
        public async Task Should_create_new_item();
        [Fact]
        public async Task Should_increment_Count_of_an_existing_item();
        [Fact]
        public async Task Should_set_the_new_Expiration_date_of_an_existing_item();
        [Fact]
        public async Task Should_set_the_Count_of_expired_items_to_1();
        [Fact]
        public async Task Should_remove_expired_items();
    }
    public class AllAsync : InMemoryWishListTest
    {
        [Fact]
        public async Task Should_return_items_ordered_by_Count_Descending();
        [Fact]
        public async Task Should_not_return_expired_items();
    }
    // Private helper methods omitted
}

Let’s analyze that code (see the source code on GitHub: https://adpg.link/ywy8). We mocked the ISystemClock interface in the tests and programmed it to obtain the desired results based on each test case. We also programmed some helper methods to make the tests easier to read. Those helpers use tuples to return multiple values (See Appendix A for more information). Here is an example of such a method setting the clock to the past:

// Mock definition:
private readonly Mock<ISystemClock> _systemClockMock = new();
// Lots of omitted code here
private (DateTimeOffset UtcNow, DateTimeOffset ExpectedExpiryTime) SetUtcNowToExpired()
{
    var delay = -(_options.ExpirationInSeconds * 2);
    var utcNow = DateTimeOffset.UtcNow.AddSeconds(delay);
    _systemClockMock.Setup(x => x.UtcNow).Returns(utcNow);
    var expectedExpiryTime = utcNow.AddSeconds(_options.ExpirationInSeconds);
    return (utcNow, expectedExpiryTime);
}

Now that we have those failing tests, here is the implementation of the InMemoryWishList class:

namespace Wishlist;
public class InMemoryWishList : IWishList
{
    private readonly InMemoryWishListOptions _options;
    private readonly ConcurrentDictionary<string, InternalItem> _items = new();
    public InMemoryWishList(InMemoryWishListOptions options)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }
    public Task<WishListItem> AddOrRefreshAsync(string itemName)
    {
        var expirationTime = _options.SystemClock.UtcNow.AddSeconds(_options.ExpirationInSeconds);
        _items
            .Where(x => x.Value.Expiration < _options.SystemClock.UtcNow)
            .Select(x => x.Key)
            .ToList()
            .ForEach(key => _items.Remove(key, out _))
        ;
        var item = _items.AddOrUpdate(
            itemName,
            new InternalItem(Count: 1,Expiration: expirationTime),
            (string key, InternalItem item) => item with { Count = item.Count + 1, Expiration = expirationTime }
            
        );
        var wishlistItem = new WishListItem(
            Name: itemName,
            Count: item.Count,
            Expiration: item.Expiration
        );
        return Task.FromResult(wishlistItem);
    }
    public Task<IEnumerable<WishListItem>> AllAsync()
    {
        var items = _items
            .Where(x => x.Value.Expiration >= _options.SystemClock.UtcNow)
            .Select(x => new WishListItem(
                Name: x.Key,
                Count: x.Value.Count,
                Expiration: x.Value.Expiration
            ))
            .OrderByDescending(x => x.Count)
            .AsEnumerable()
        ;
        return Task.FromResult(items);
    }
    private record class InternalItem(int Count, DateTimeOffset Expiration);
}

The InMemoryWishList class uses ConcurrentDictionary<string, InternalItem> internally to store the items and make the wishlist thread-safe. It also uses a with expression to manipulate and copy the InternalItem record class.

The AllAsync method filters out expired items, while the AddOrRefreshAsync method removes expired items. This might not be the most advanced logic ever, but that does the trick.

Exercise

You might have noticed the code is not the most elegant of all code, and I left it this way on purpose. While using the test suite, I invite you to refactor the methods of the InMemoryWishList class into more readable code.

I took a few minutes to refactor it myself and saved it as InMemoryWishListRefactored. You can also uncomment the first line of InMemoryWishListTest.cs to test that class instead of the main one. My refactoring is a way to make the code cleaner, to give you ideas. It is not the only way, nor the best way, to write that class (the “best way” being subjective).

Back to DI, the line that makes the wishlist shared between users is in the composition root that we explored earlier. As a reference, here it is:

builder.Services.AddSingleton<IWishList, InMemoryWishList>();

Yes, only that line makes all the difference between creating multiple instances and a single shared instance. By setting the lifetime to Singleton, you can open multiple browsers and share the wishlist.

To POST to the API, I recommend using the Postman collection (https://adpg.link/postman6) that comes with the book. The collection already contains multiple requests that you can execute in batches or individually. You can also use the Swagger UI that I added to the project. I left the code out of the chapter as it was not useful, but that’s the default URL that should open when starting the project. If you prefer working in a terminal, you can use curl or Invoke-WebRequest, depending on your OS.

That’s it! All that code to demo what a single line can do, and we have a working program, as tiny as it is.

Conclusion

This section explored how to replace the classic Singleton pattern with a class registered with a singleton lifetime. We looked at the old application state, learned that was no more, and implemented two versions of it. We no longer need that, but it was a good way of learning about singletons.

We then implemented a wishlist system as a second example and concluded that the whole thing was working due to, and managed by, a single line of the composition root: the call to the AddSingleton method. Changing that line could drastically change the behavior of the system, even making it unusable.

From now on, you can see the Singleton pattern as an anti-pattern in .NET, and unless you find strong reasons to implement it, you should stick to normal classes and DI instead. Doing this moves the creation responsibility from the singleton class to the composition root, which is the composition root’s responsibility, leaving the class only one responsibility: perfectly in line with the Single Responsibility Principle.

Next, we explore the Service Locator anti-pattern/code smell.

Understanding the Service Locator pattern

Service Locator is an anti-pattern that reverts the IoC principle to its Control Freak roots. The only difference is using the IoC container to build the dependency tree instead of the new keyword.

There is some use of this pattern in ASP.NET, and some may argue that there are some reasons for one to use the Service Locator pattern, but it should happen very rarely or never. For that reason, in this book, let’s call Service Locator a code smell instead of an anti-pattern.

The DI container uses the Service Locator pattern internally to find dependencies, which is the correct way of using it. In your applications, you want to avoid injecting an IServiceProvider to get the dependencies you need from it, which revert to the classic flow of control.

My strong recommendation is don’t use Service Locator unless you know what you are doing and have no other option. A good use of Service Locator could be to migrate a legacy system that is too big to rewrite. So you could build the new code using DI and update the legacy code using the Service Locator pattern, allowing both systems to live together or migrate one into the other, depending on your goal.

Project – ServiceLocator

The best way to avoid something is to know about it, so let’s see how to implement the Service Locator pattern using IServiceProvider to find a dependency.

The service we want to use is an implementation of IMyService. Let’s start with the interface:

namespace ServiceLocator;
public interface IMyService : IDisposable
{
    void Execute();
}

The interface implements the IDisposable interface and contains a single Execute method. Here is the implementation, which does nothing more than throw an exception if the instance has been disposed of:

namespace ServiceLocator;
public class MyServiceImplementation : IMyService
{
    private bool _isDisposed = false;
    public void Dispose() => _isDisposed = true;
    public void Execute()
    {
        if (_isDisposed)
        {
            throw new NullReferenceException("Some dependencies have been disposed.");
        }
    }
}

Then, let’s add a controller that implements the Service Locator pattern:

namespace ServiceLocator;
public class MyController : ControllerBase
{
    private readonly IServiceProvider _serviceProvider;
    public MyController(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }
    [Route("/service-locator")]
    public IActionResult Get()
    {
        using var myService = _serviceProvider.GetRequiredService<IMyService>();
        myService.Execute();
        return Ok("Success!");
    }
}

In that code sample, instead of injecting IMyService into the constructor, we are injecting IServiceProvider. Then, we use it (highlighted line) to locate the IMyService instance. Doing so shifts the responsibility for creating the object from the container to the consumer (MyController, in this case). MyController should not be aware of IServiceProvider and should let the container do its job without interference.

What could go wrong? If we run the application and navigate to /service-locator, everything works as expected. However, if we reload the page, we get an error thrown by the Execute() method because we called Dispose() during the previous request. MyController should not control its injected dependencies, which is the point that I am trying to emphasize here: leave the container to control the lifetime of dependencies rather than trying to be a control freak. Using the Service Locator pattern opens pathways toward those wrong behaviors, which will most likely cause more harm than good in the long run.

Moreover, even though the ASP.NET Core container does not natively support this, we lose the ability to inject dependencies contextually when using the Service Locator pattern because the consumer controls its dependencies. What do I mean by contextually? One could inject instance A into a class but instance B into another class.

Before exploring ways to fix this, here is the Program.cs code that powers this program:

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<IMyService, MyServiceImplementation>()
    .AddControllers()
;
var app = builder.Build();
app.MapControllers();
app.Run();

There is nothing fancy in the preceding code apart from enabling controller support and registering our service. To fix the controller, we need to either remove the using statement or move away from Service Locator and inject our dependencies instead. I picked moving away from the Service Locator pattern, and we will tackle the following:

  • Method injection
  • Constructor injection
  • Minimal API

Implementing method injection

Let’s start by using method injection to demonstrate its use:

public class MethodInjectionController : ControllerBase
{
    [Route("/method-injection")]
    public IActionResult GetUsingMethodInjection([FromServices]IMyService myService)
    {
        ArgumentNullException.ThrowIfNull(myService, nameof(myService));
        myService.Execute();
        return Ok("Success!");
    }
}

Let’s analyze the code:

  • The FromServicesAttribute class tells the model binder about method injection. We can inject zero or more services into any action by decorating its parameters with [FromServices].
  • We added a guard clause to protect us from null. We leverage the ThrowIfNull method instead of manually checking for null then throwing an exception (see Appendix A).
  • Finally, we kept the original code except for the using statement.

    Note

    Method injection like this would be of good use for a controller with multiple actions, but that uses IMyService in only one of them.

Implementing constructor injection

Let’s continue by implementing the same solution using constructor injection. Our new controller looks like this:

namespace ServiceLocator;
public class ConstructorInjectionController : ControllerBase
{
    private readonly IMyService _myService;
    public ConstructorInjectionController(IMyService myService)
    {
        _myService = myService ?? throw new ArgumentNullException(nameof(myService));
    }
    [Route("/constructor-injection")]
    public IActionResult GetUsingConstructorInjection()
    {
        _myService.Execute();
        return Ok("Success!");
    }
} 

When using constructor injection, we ensure that IMyService is not null upon class instantiation. Since it is a class member, it is even less tempting to call its Dispose() method in an action, leaving that responsibility to the container (as it should be).

Both techniques are an acceptable replacement for the Service Locator anti-pattern. Let’s analyze the code before moving to the next possibility:

  • We implemented the strategy pattern with constructor injection.
  • We added a guard clause to ensure that no null value could get in at runtime.
  • We simplified the action to what it should do: to a bare minimum.

Implementing a minimal API

Minimal APIs allow us to implement method injection the same way we did previously but without creating a controller. This approach is very beneficial for code samples, educational material, and APIs representing a very thin controller layer. Here is the code to add to the Program.cs file:

app.MapGet("/minimal-api", (IMyService myService) =>
{
    myService.Execute();
    return "Success!";
});

That code does the same as the method injection sample without the guard clause that I omitted because no external consumer is likely to inject nulls into it: the endpoint is a delegate that is passed directly to the MapGet method. In other cases, where your delegates can be called by external code, you could add a guard clause there as well.

Conclusion

Most of the time, by following the Service Locator anti-pattern, we only hide the fact that we are taking control of objects instead of decoupling our components. The last example demonstrated a problem when disposing of an object, which could happen using constructor injection. However, when thinking about it, it is more tempting to dispose of an object that we create than one that is injected.

Moreover, the service locator takes control away from the container and moves it into the consumer, against the Open-Closed Principle. You should be able to update the consumer by updating the composition root’s bindings. In this case, we could change the binding, and it would work. In a more advanced case, when requiring contextual injection, we would have difficulty binding two implementations to the same interface, one for each context; it could even be impossible.

The IoC container is responsible for weaving the program’s thread, connecting its pieces together where each independent piece should be as clueless as possible about the others.

This anti-pattern also complicates testing. When unit testing your class, you need to mock a container that returns a mocked service instead of mocking only the service.

One place where I can see its usage justified is in the composition root, where bindings are defined, and sometimes, especially when using the built-in container, we can’t avoid it. Another place would be a library that adds functionalities to the container. Other than that, try to stay away!

Beware

Moving the service locator elsewhere does not make it disappear; it only moves it around, like any dependency.

Next, we revisit our third and final pattern of this chapter.

Revisiting the Factory pattern

In the Strategy pattern example, we implemented a solution that instantiated HomePageViewModel using the new keyword. While doing that is acceptable, we could use method injection instead, mixed with the use of a factory. The Factory patterns are handy tools when the time comes to construct objects. Let’s look at a few rewrites of the Strategy project using factories to explore some possibilities.

Project – Factory

Let’s start by mixing a factory with method injection and injecting the view model directly into our method instead of injecting IHomeService. To achieve this, we rewrite the Index method of the HomeController class to this:

public class HomeController : Controller
{
    public IActionResult Index([FromServices]HomePageViewModel viewModel)
    {
        return View(viewModel);
    }
    // Omitted Privacy() and Error()
}

The FromServicesAttribute class tells the ASP.NET Core pipeline to inject an instance of HomePageViewModel directly into the method. Unfortunately, the IoC container is not yet aware of how to create such an instance. Now, we use a factory instead of a static binding to explain to the container what to do. Here is the Project.cs file (factory highlighted):

using Factory.Models;
using Factory.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<IHomeService, HomeService>()
    .AddTransient(serviceProvider =>
    {
        var homeService = serviceProvider.GetRequiredService<IHomeService>();
        var data = homeService.GetHomePageData();
        return new HomePageViewModel(data);
    })
    .AddSingleton<IHomeViewModelFactory, HomeViewModelFactory>()
    .AddControllersWithViews()
;
var app = builder.Build();
app.MapDefaultControllerRoute();
app.Run();

In the preceding code, we used another overload of the AddTransient() extension method and passed Func<IServiceProvider, TService> implementationFactory as an argument. The highlighted code represents our factory, and that factory is implemented as a service locator using the IServiceProvider instance to create the IHomeService dependency that we use to instantiate HomePageViewModel.

We are using the new keyword here, but is this wrong? The composition root is where elements should be created (or configured), so instantiating objects there is okay, as it is to use the Service Locator pattern. However, you should aim to avoid it whenever possible. It is harder to avoid when using the default container versus a full-featured third-party one, but we can avoid it in many cases.

We could also create a factory class to keep our composition root clean as we do with the HomeViewModelFactory soon. While that is true, we would only move the code around, adding more complexity to our program. That is the reason why creating view models inside your controller’s actions is acceptable to reduce unnecessary complexity.

Moreover, creating view models inside actions should not negatively impact the program’s maintainability in most cases since a view model is bound to a single view, controlled by a single action, leading to a one-on-one relationship. Furthermore, it is way cheaper to implement, and it is also easier to understand than roaming around the code to find what binding does what. However, the biggest downside of instantiating view models manually in the action method is testability. It is easier to inject the data we want from a test case than dealing with hardcoded object creation. Nonetheless, the extra complexity that we are implementing here is rarely worth it for a view model but is a good way to demonstrate the use of factories.

We could create a class to handle the factory logic for more complex scenarios or to clean the composition root. We could also create a class and an interface to directly use the factory inside our other classes. This approach comes in handy for creating your dependencies only when you need them; this is also known as lazy loading. Lazy loading means to create the object only when needed, deferring the overhead of doing so to the time of use or during its first use.

Note

There is an existing Lazy<T> class to help with lazy loading, but that is not the point of this code sample. The idea is the same, though: we create the object only when first needed.

Unless you need to reuse that creation logic in multiple places, creating a factory class may not be worth it. Nevertheless, this is a convenient pattern worth remembering. Here is how to implement a factory that returns an instance of PrivacyViewModel:

namespace Factory.Services;
public interface IHomeViewModelFactory
{
    PrivacyViewModel CreatePrivacyViewModel();
}
public class HomeViewModelFactory : IHomeViewModelFactory
{
    public PrivacyViewModel CreatePrivacyViewModel() => new()
    {
    Title = "Privacy Policy (from IHomeViewModelFactory)",
    Content = new HtmlString("<p>Use this page to detail your site's privacy policy.</p>")
    };
}

The preceding code encapsulates the creation of PrivacyViewModel instances into HomeViewModelFactory. The code is very basic; it creates an instance of the PrivacyViewModel class and fills its properties with hardcoded values.

Now, to use that new factory, we update the controller, use constructor injection to inject IHomeViewModelFactory into HomeController, and then use it from the Privacy() action method, like this:

private readonly IHomeViewModelFactory _homeViewModelFactory;
public HomeController(IHomeViewModelFactory homeViewModelFactory)
{
    _homeViewModelFactory = homeViewModelFactory ?? throw new ArgumentNullException(nameof(homeViewModelFactory));
}
// Omitted action methods
public IActionResult Privacy()
{
    var viewModel = _homeViewModelFactory.CreatePrivacyViewModel();
    return View(viewModel);
}

The preceding code is clear, simple, and easy to test.

By using this technique, we are not limited to one method. We can write multiple methods that each encapsulate their own creational logic in the same factory. We could also pass additional objects to the Create[object to create]() method (highlighted), like this:

public HomePageViewModel CreateHomePageViewModel(IEnumerable<string> someData)
    => new(someData);

The possibilities are almost endless when you think about it, so now that you’ve seen a few in action, you may find other uses for a factory when you need to inject some classes with complex instantiation logic into other objects.

One advantage of using factories like this is to inject dependencies into them and use them for that complex instantiation logic. For example, you could inject a class that connects to a database and leverage that data to build the object. Without a factory, the controller (in our case) would have that added responsibility.

Conclusion

The use of a factory can be considered for multiple scenarios. Our example covered the fact that we could create a factory in the composition root to instantiate dependencies (a Func<IServiceProvider, TService>). We leveraged method injection to get that view model instance, and we saw that sometimes, the new keyword should be used directly instead of trying to implement more complex code that would, in the end, only move the problem around, leading to false decoupling with added complexity.

As a rule of thumb, creating view models in controller actions is acceptable, and classes containing logic are the ones we want to inject to break tight coupling with. Just keep in mind that moving code around your codebase does not make that code, logic, dependencies, or coupling disappear.

Moreover, we explored the creation of a factory class backed by an interface. Those are very helpful for more complex instantiation, removing inlined factories from the composition root, or depending on other services, like access to a database.

We use factories often, and now that you know more about them, I’m sure you’ll start to see them in the .NET APIs if you have not already.

Summary

This chapter covered the basics of Dependency Injection and how to leverage it to follow the Dependency Inversion Principle helped by the Inversion of Control principle. We then revisited the Strategy design pattern, and saw how to use it to create a flexible, DI-ready system. We also revisited the Singleton pattern, seeing that we can inject the same instance, system-wide, by using the singleton lifetime when configuring dependencies in the container. We finally saw how to leverage factories to handle complex object creation logic.

We also talked about moving code around, the illusion of improvement, and the cost of engineering. We saw that the new keyword could help reduce complexity when the construction logic is simple enough and that it could save time and money. On the other hand, we also explored a few techniques to handle object creation complexity to create maintainable and testable programs, such as factories, and get away from the Control Freak code smell. We also visited the guard clause, which guards our injected dependencies against null. This way, we can demand some required services using constructor injection and use them from the class methods without testing for null every time.

We explored how the Service Locator (anti-)pattern can be harmful and how it can be leveraged from the composition root to create complex objects dynamically. We also discussed why to keep its usage frequency as close to never as possible. Understanding these patterns and code smells is very important when it comes to keeping your systems maintainable as your programs grow in size. For programs that require complex DI logic, conditional injection, multiple scopes, auto-implemented factories, and other advanced features, we saw that it is possible to use third-party containers instead of the built-in one.

In subsequent sections, we will explore tools that add functionality to the default container, reducing the need to swap it for another. If you are building multiple smaller projects (microservices) instead of a big one (a monolith), that may save you from requiring those extra features, but nothing is free, and everything has a cost; more on this in Chapter 16, Introduction to Microservices Architecture.

In the next chapter, we are going to explore options and logging patterns. These ASP.NET Core patterns aim to make our lives easier when managing such common problems.

Questions

Let’s take a look at a few practice questions:

  1. What are the three DI lifetimes that we can assign to objects in ASP.NET Core?
  2. What is the composition root for?
  3. Is it true that we should avoid the new keyword when instantiating volatile dependencies?
  4. What is the pattern that we revisited in this chapter that helps compose objects to eliminate inheritance?
  5. Is the Service Locator pattern a design pattern, a code smell, or an anti-pattern?

Further reading

Here are some links to build upon what we have learned in the chapter:

Join our book’s Discord space

Join the book’s Discord workspace for Ask me Anything session with the authors:

https://packt.link/ASPdotNET6DesignPatterns

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

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