Dependency Injection Life Cycle Truth: The More Secrets You Know!

Dependency Injection (DI) is a pattern that can help developers decouple the different pieces of their applications. “Dependency Injection” is a 25-dollar term for a 5-cent concept. It means giving an object its instance variables. Really. That’s it.

Classes have these things they call methods on. Let’s call those “dependencies.” Most people call them “variables.” Sometimes, when they’re feeling fancy, they call them “instance variables.”

public class Example {
private DatabaseThingie myDatabase;

  public Example() {
myDatabase = new DatabaseThingie();
  }

  public void DoStuff() {
    ...
myDatabase.GetData();
    ...
  }
}

Here, we have a variable (dependency); named “myDatabase.”
We initialize it in the constructor.

Dependency Injection
Dependency Injection

Dependency Injection

If we wanted to, we could pass the variable into the constructor. That would “inject” the “dependency” into the class. Now when we use the variable (dependency), we use the object that we were given rather than the one we created.

public class Example
    {
        private DatabaseThingie myDatabase;
        public Example()
        {
            myDatabase = new DatabaseThingie();
        }
        public Example(DatabaseThingie useThisDatabaseInstead)
        {
            myDatabase = useThisDatabaseInstead;
        }
        public void DoStuff()
        {
            ...
            myDatabase.GetData();
            ...
        }
    }

That’s really all there is to it. The rest is just variations on the theme. We could set the dependency (<cough> variable) in… wait for it… a setter method! We can set the dependency by calling a setter method that we define in a special interface. Or, we can have the dependency be an interface and then polymorphically pass in some polyjuice; or something.

Dependency injection is a software design pattern that enables a class to receive its dependencies from external sources rather than creating them itself. It helps to decouple the different pieces of a software application and makes it easier to maintain and test.

In dependency injection, a class specifies the dependencies it requires through its constructor or other methods, and an external entity called an “injector” is responsible for providing the dependencies to the class. The injector typically uses a configuration file or other means to determine which dependency to provide for a given request.

There are several benefits to using dependency injection in software development:

  1. It allows for better separation of concerns, as each class only needs to worry about its own functionality rather than how to create and manage its dependencies.
  2. It makes it easier to test individual classes, as mock dependencies can be provided to the class under test rather than relying on actual implementations.
  3. It enables more flexible and modular design, as different implementations of dependencies can be easily swapped in and out.
  4. It can improve the overall performance and scalability of a software application by allowing dependencies to be shared and reused.

There are several different approaches to implementing dependency injection, including constructor injection, setter injection, and interface injection. The choice of approach often depends on the specific requirements and constraints of the software application.

At registration time, dependencies require a lifetime definition. The service lifetime defines the conditions under which a new service instance will be created. Let’s see what .NET Core DI framework defined lifetimes are.

  1. Transient
    Created every time they are requested
  2. Scoped
    Created once per scope; i.e., web request.or any unit of work
  3. Singleton
    Created only for the first request. If a particular instance is specified at registration time, this instance will be provided to all consumers of the registration type.

Transient Lifetime

If in doubt, make it transient. That’s really what it comes down to. Adding a transient service means that each time the service is requested, a new instance is created.

In the example below, we have created a simple service named “MyService” and added an interface. We register the service as transient and ask for the instance twice. In this case we are asking for it manually, but in most cases we will be asking for the service in the constructor of a controller/class.

public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<IMyService, MyService>();

	var serviceProvider = services.BuildServiceProvider();

	var instanceOne = serviceProvider.GetService<IMyService>();
	var instanceTwo = serviceProvider.GetService<IMyService>();

	Debug.Assert(instanceOne != instanceTwo);
}

This passes with flying colors.
The instances are not the same and the .net core DI framework creates a new instance each time. If we were creating instances of services manually in our code without a DI framework, then transient lifetime is going to be pretty close to a drop in.

One thing that I should add is that there was a time when it was all the rage to stop using Transient lifetimes, and try and move towards using singletons. The thinking was that instantiating a new instance each time a service was requested was a performance hit. But this only happened on huge monoliths with massive/complex dependency trees. The majority of cases trying to avoid Transient lifetimes ended up breaking functionality because using Singletons didn’t function how they thought it would. If we are having performance issues, may be we should look elsewhere.

Singleton Lifetime

A singleton is an instance that will last the entire lifetime of the application. In web terms, it means that after the initial request of the service, every subsequent request will use the same instance. This also means it spans across web requests (So if two different users hit your website, the code still uses the same instance). The easiest way to think of a singleton is if we have a static variable in a class, it is a single value across multiple instances.

Using our example from above :

public void ConfigureServices(IServiceCollection services)
{
	services.AddSingleton<IMyService, MyService>();

	var serviceProvider = services.BuildServiceProvider();

	var instanceOne = serviceProvider.GetService<IMyService>();
	var instanceTwo = serviceProvider.GetService<IMyService>();

	Debug.Assert(instanceOne != instanceTwo);
}

We are now adding our service as a singleton and our Assert statement from before now blows up because the two instances are actually the same!

Now why would we ever want this? For the most part, it’s great to use when we need to “share” data inside a class across multiple requests because a singleton holds “state” for the lifetime of the application. The best example was when we need to “route” requests in a round robin type fashion. Using a singleton, we can easily manage this because every request is using the same instance.

Scoped Lifetime

Scoped lifetime objects often get simplified down to “one instance per web request”, but it’s actually a lot more nuanced than that. Admittedly in most cases, we can think of scoped objects being per web request. So common things we might see is a DBContext being created once per web request, or NHibernate contexts being created once so that we can have the entire request wrapped in a transaction. Another extremely common use for scoped lifetime objects is when we want to create a per request cache.

Scoped lifetime actually means that within a created “scope” objects will be the same instance. It just so happens that within .net core, it wraps a request within a “scope”, but we can actually create scopes manually. For example :

public void ConfigureServices(IServiceCollection services)
{
	services.AddScoped<IMyScopedService, MyScopedService>();

	var serviceProvider = services.BuildServiceProvider();

	var serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();

	IMyScopedService scopedOne;
	IMyScopedService scopedTwo;

	using (var scope = serviceScopeFactory.CreateScope())
	{
		scopedOne = scope.ServiceProvider.GetService<IMyScopedService>();
	}

	using (var scope = serviceScopeFactory.CreateScope())
	{
		scopedTwo = scope.ServiceProvider.GetService<IMyScopedService>();
	}


	Debug.Assert(scopedOne != scopedTwo);
}

In this example, the two scoped objects aren’t the same because created each object within their own “scope”. Typically in a simple .net core CRUD API, we aren’t going to be manually creating scopes like this. But it can come to the rescue in large batch jobs where you want to “ditch” the scope each loop for example.

Dependency Injection Good Practices

  • Register your services as transient wherever possible. Because it’s simple to design transient services. You generally don’t care about multi-threading and memory leaks and you know the service has a short life.
  • Use scoped service lifetime carefully since it can be tricky if you create child service scopes or use these services from a non-web application.
  • Use singleton lifetime carefully since then you need to deal with multi-threading and potential memory leak problems.
  • Do not depend on a transient or scoped service from a singleton service. Because the transient service becomes a singleton instance when a singleton service injects it and that may cause problems if the transient service is not designed to support such a scenario. ASP.NET Core’s default DI container already throws exceptions in such cases.

Dependency injection is a programming design pattern in which an object receives other objects that it depends on. These objects are called dependencies. Instead of creating the dependencies directly within the class, they are “injected” into the class through a constructor or method. This allows for more flexibility and modularity in the code, as the class can work with any object that satisfies the required interface. There are different service lifetimes that can be defined in dependency injection frameworks, including transient, scoped, and singleton. Transient means that a new instance is created every time it is requested, while scoped means that a new instance is created once per unit of work (such as a web request). Singleton means that only one instance is created for the entire application and is used for all requests. Singleton is generally not recommended for most cases as it can lead to unexpected behavior and difficulties in testing.

Read More On: Dependency injection in ASP.NET Core | Microsoft Learn

Leave a Comment