Dependency injection can be a tricky thing to wrap our brains around. We use libraries for it because understanding how dependency injection really works takes a lot more effort than just using dependency injection.
I’m going to try to keep this post really short so we can use it as a reference, but if we want to know more about dependency injections and libraries that do dependency injection for us we can read…
- Dependency injection in ASP.NET Core (general knowledge about dependency injection)
- Autofac documentation (dependency injection library)
- SimpleInjector documentation (dependency injection library)
- .NET dependency injection (dependency injection library)
What is Dependency Injection?
“Dependency Injection” is a fancy way to say “There’s some thing that knows about the objects my classes and methods depend on. That thing can create, track and destroy those objects for us and ‘inject’ them so that we don’t have to worry about them as much.”
In this post, we’ll refer to the thing as “Framework”.
What are Lifetimes?
A Lifetime is the rule for when the Framework creates, tracks and destroys instances of classes that it knows about. The most common lifetimes are:
- Transient: Any time a class is created that depends on an instance of this class, create a new instance of this class.
- Scoped: Any time there’s a new scope and a class is created that depends on an instance of this class, create 1 instance of this class and use that for any other classes that need this class in this scope.
- Singleton: There is only 1 instance of this class while this application is running and it is THIS instance so any time a class is created that depends on an instance of this class, use this existing instance.
Transient
Any time a class is created that depends on an instance of this class, create a new instance of this class.
Code example:
(In Startup.cs using .NET dependency injection...)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IDoThingsService, DoThingsThisWayService>();
(In other files...)
public interface IDoThingsService
{
//fun methods that this interface defines
}
public class DoThingsThisWayService : IDoThingsService
{
//implementations of the methods defined by IDoThingsService
}
public class SomeClass
{
private readonly IDoThingsService _service;
public SomeClass(IDoThingsService service)
{
_service = service;
}
}
English translation:
“Hey Framework! Any time I create an object that is a SomeClass
that needs an IDoThingsService
, create a brand new DoThingsThisWayService
and use that. When the SomeClass
object gets destroyed, also destroy whatever IDoThingsService
we created.”
Scoped
Any time there’s a new scope and a class is created that depends on an instance of this class, create 1 instance of this class and use that for any other classes that need this class in this scope.
If we read that sentence carefully, we’re gonna realize that the word “scope” doesn’t really mean anything to us. This is by design because a “scope” can be anything we want. For web applications, the scope is usually a client request.
Code example:
(In Startup.cs using .NET dependency injection...)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IDoThingsService, DoThingsThisWayService>();
(In other files...)
public interface IDoThingsService
{
//fun methods that this interface defines
Task<DoThingResponse> DoTheThing(SomeWebThingRequest request);
}
public class DoThingsThisWayService : IDoThingsService
{
//implementations of the methods defined by IDoThingsService
public async Task<DoThingResponse> DoTheThing(SomeWebThingRequest request)
{
//fancy code that does something asynchronously and returns a DoThingResponse
}
}
public class SomeController
{
private readonly IDoThingsService _service;
public SomeController(IDoThingsService service)
{
_service = service;
}
public async Task<IActionResult> DoSomeWebThing(SomeWebThingRequest request)
{
var response = await _service.DoTheThing(request);
if(/* response is good */)
{
return OK(/* response is good */);
}
return BadRequest(/* response is bad*/);
}
}
English translation for web applications:
“Hey Framework! Any time this API endpoint is called and we create an object that is a SomeController
that needs an IDoThingsService
, create a brand new DoThingsThisWayService
and use that. When the
object gets destroyed, keep the SomeController
IDoThingsService
we created until we’re done with the API request in case something else needs it during this scope. Once we’re done with the API request, then destroy the IDoThingsService
.”
Singleton
There is only 1 instance of this class while this application is running and it is THIS instance so any time a class is created that depends on an instance of this class, use this existing instance.
Code example:
(In Startup.cs using .NET dependency injection...)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDoThingsService, DoThingsThisWayService>();
(In other files...)
public interface IDoThingsService
{
//fun methods that this interface defines
}
public class DoThingsThisWayService : IDoThingsService
{
//implementations of the methods defined by IDoThingsService
}
public class SomeClass
{
private readonly IDoThingsService _service;
public SomeClass(IDoThingsService service)
{
_service = service;
}
}
public class SomeOtherClass
{
private readonly IDoThingsService _service;
public SomeOtherClass (IDoThingsService service)
{
_service = service;
}
}
English translation:
“Hey Framework! The first time I create an object that needs an IDoThingsService
, create a brand new DoThingsThisWayService
and use that then keep that instance of DoThingsThisWayService
because we’re not making any more new ones. We’re using that instance forever! Well, at least as long as the application is still running.”