Dependency Injection Life Cycle: The more 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.

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 seems simple to use at first, but there are potential multi-threading and memory leak problems if we don’t follow some strict principles.