Within this tutorial, you will learn how to master dependency injection within Umbraco v9. By the time that you have finished with this guide, you will have learnt two important concepts. First, you will learn all about the different scopes that you can use and when you should use them. Second, you will learn how to automagically hook everything up so you will no longer need to manually register anything yourself.
By installing a third-party package, you can skip the pain of constantly having to register your custom code with the container, saving you time and your sanity. If you want to never have to worry about configuring a container ever again, read on 🔥🔥🔥
Within v9, Umbraco uses the default .NET container that comes with .NET Core as the mechanism to provide dependency injection. This is a change from the last version.
Within V8, Umbraco used to use SimpleInjector to manage dependency injection. This change in the container type means that when you upgrade from a previous version to v9, your registration code will break and you will need to fix it. The good news is that these refactoring tasks will be fairly easy, however, it will be another to-do item to add to your ever-growing list 😞
An important part of dependency injection is configuring how the container should instantiate an object whenever a request is made. In essence, the strategies are all based on how you want to manage the object's state. When an object is created its internal properties can change over time. Sometimes you might want this state to be shared between different requests, while at other times you might want the object to be instantiated from scratch each time. As you may need to manage state differently, the DI container has three different ways of configuring dependencies.
Scoped: Scoped lifetime services are objects that live for the entirety of a request. A scoped object will exist until the current HTTP context is destroyed. The reason to pick scoped requests is when you want to maintain the state throughout the entirety of a single request.
Singleton: Singleton scoped dependencies are objects that are created once during the lifetime of an application and then shared until the server is reset or turned off. Use a singleton scope whenever you need to maintain an object's state application-wide. The two classic examples of features to inject using a singleton are logging and caching providers.
Speaking from personal experience, a singleton scope is the riskiest type of scope that you can use, so I advise that you use it sparingly. Singletons can lead to odd state management bugs that are hard to spot. These bugs are usually caused when singleton scoped dependencies are injected into other transient dependencies. If you inject a singleton scope into a transient (covered shortly) you may expect no state management to occur, however, that is not the case and spotting these bugs can be really hard.
Transient: In a transient scoped dependency, the container will create a new object each time it is requested. Since the objects are created as new every time state management is not a problem. This is why transient scopes are the safest to use and they are recommended as best practice. Transient scopes will use slightly more memory and resources, however, I've never encountered any issues using all transient within high profile/high-traffic websites.
Everyone knows the classic IT line, turn it on and off again to fix things. Starting from a blank slate reduces the chance of a previous state breaking shit. A transient scope is good because it means all your dependencies will be created like new each time, which in practice will help reduce odd bugs from occurring.
Auto-Discovery and Auto Registration
One boring part of dependency injection is the constant need to register all the new classes that you write with the container. One way to make your life a lot easier is through auto-discovery and registration. As the name implies, auto-discovery will tell the container to try and find dependencies for you on the application start-up. Auto-discovery and registration can significantly cut down on the amount of config code that you have to write. To make this work, you need to tell the container about all the class libraries that you want it to scan. This is done using reflection.
One big issue with auto-discovery and Umbraco is that the Microsoft DI .NET Core container does not support auto-registration out of the box. It was deliberately built to be minimal and non-opinionated. The good news is that auto-discovery and auto registration are still possible with the .NET Core DI container using a third-party package. There are a few packages on the market that add this capability to the Microsoft container. The one that I recommend is a free package called Scrutor. After installing Scrutor via NuGet, you can then create a Umbraco composer to configure the DI container:
It is important that you understand what this code does, so let us break things down line by line:
The first line,
FromAssemblies(), tells Scrutor what libraries to include in the scan. The easiest way to register your custom class libraries within this call is via reflection. This is where the use of
typeof() line comes into play. To register a library simply pass any class or interface contained within that library into
typeof(). Using the
Assembly property will give you access to the assembly that it was created from.
.AddClasses() tells the plugin to scan all the classes contained within the class library for potential auto-registration
You may still want to do some manual configuration within the container. Setting
RegistrationStrategy.Skip means if the engine has already registered a type, ignore it. Duplication can also occur if the engine encounters the same interface being injected into different classes within the same class library. When the engine spots the same interface a duplication exception can be thrown. This line prevents that exception from occurring.
This line defines the auto-registration rule. The
AsImplementedInterfaces flag means that when the container finds an interface
IMyClass it will try to automatically instantiate a class named
Configure anything found within the auto-discovery process as a transient scope (the recommended scope to use).
You are now a complete dependency injection master 💥 Following SOLID principles will help you build a system that will be easier to maintain and manage. By using Scrutor for auto-discovery and auto registration, you will get the benefits of dependency injection without the hassle of managing the configuration. Huzzah! Happy Coding 🤘