Understand dependency injection basics in .NET
In this article, you create a .NET console app that manually creates a ServiceCollection and corresponding ServiceProvider. You learn how to register services and resolve them using dependency injection (DI). This article uses the Microsoft.Extensions.DependencyInjection NuGet package to demonstrate the basics of DI in .NET.
Note
This article doesn't take advantage of the Generic Host features. For a more comprehensive guide, see Use dependency injection in .NET.
Get started
To get started, create a new .NET console application named DI.Basics. Some of the most common approaches for creating a console project are referenced in the following list:
- Visual Studio: File > New > Project menu.
- Visual Studio Code and the C# Dev Kit extension's: Solution Explorer menu option.
- .NET CLI:
dotnet new console
command in the terminal.
You need to add the package reference to the Microsoft.Extensions.DependencyInjection in the project file. Regardless of the approach, ensure the project resembles the following XML of the DI.Basics.csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
</ItemGroup>
</Project>
Dependency injection basics
Dependency injection is a design pattern that allows you to remove hard-coded dependencies and make your application more maintainable and testable. DI is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.
The abstractions for DI in .NET are defined in the Microsoft.Extensions.DependencyInjection.Abstractions NuGet package:
- IServiceCollection: Defines a contract for a collection of service descriptors.
- IServiceProvider: Defines a mechanism for retrieving a service object.
- ServiceDescriptor: Describes a service with its service type, implementation, and lifetime.
In .NET, DI is managed by adding services and configuring them in an IServiceCollection
. After services are registered, an IServiceProvider
instance is built by calling the BuildServiceProvider method. The IServiceProvider
acts as a container of all the registered services, and it's used to resolve services.
Create example services
Not all services are created equally. Some services require a new instance each time that the service container gets them (transient), while others should be shared across requests (scoped) or for the entire lifetime of the app (singleton). For more information on service lifetimes, see Service lifetimes.
Likewise, some services only expose a concrete type, while others are expressed as a contract between an interface and an implementation type. You create several variations of services to help demonstrate these concepts.
Create a new C# file named IConsole.cs and add the following code:
public interface IConsole
{
void WriteLine(string message);
}
This file defines an IConsole
interface that exposes a single method, WriteLine
. Next, create a new C# file named DefaultConsole.cs and add the following code:
internal sealed class DefaultConsole : IConsole
{
public bool IsEnabled { get; set; } = true;
void IConsole.WriteLine(string message)
{
if (IsEnabled is false)
{
return;
}
Console.WriteLine(message);
}
}
The preceding code represents the default implementation of the IConsole
interface. The WriteLine
method conditionally writes to the console based on the IsEnabled
property.
Tip
The naming of an implementation is a choice that your dev-team should agree on. The Default
prefix is a common convention to indicate a default implementation of an interface, but it's not required.
Next, create an IGreetingService.cs file and add the following C# code:
public interface IGreetingService
{
string Greet(string name);
}
Then add a new C# file named DefaultGreetingService.cs and add the following code:
internal sealed class DefaultGreetingService(
IConsole console) : IGreetingService
{
public string Greet(string name)
{
var greeting = $"Hello, {name}!";
console.WriteLine(greeting);
return greeting;
}
}
The preceding code represents the default implementation of the IGreetingService
interface. The service implementation requires an IConsole
as a primary constructor parameter. The Greet
method:
- Creates a
greeting
given thename
. - Calls the
WriteLine
method on theIConsole
instance. - Returns the
greeting
to the caller.
The last service to create is the FarewellService.cs file, add the following C# code before continuing:
public class FarewellService(IConsole console)
{
public string SayGoodbye(string name)
{
var farewell = $"Goodbye, {name}!";
console.WriteLine(farewell);
return farewell;
}
}
The FarewellService
represents a concrete type, not an interface. It should be declared as public
to make it accessible to consumers. Unlike other service implementation types that were declared as internal
and sealed
, this code demonstrates that not all services need to be interfaces. It also shows that service implementations can be sealed
to prevent inheritance and internal
to restrict access to the assembly.
Update the Program
class
Open the Program.cs file and replace the existing code with the following C# code:
using Microsoft.Extensions.DependencyInjection;
// 1. Create the service collection.
var services = new ServiceCollection();
// 2. Register (add and configure) the services.
services.AddSingleton<IConsole>(
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
});
services.AddSingleton<IGreetingService, DefaultGreetingService>();
services.AddSingleton<FarewellService>();
// 3. Build the service provider from the service collection.
var serviceProvider = services.BuildServiceProvider();
// 4. Resolve the services that you need.
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
var farewellService = serviceProvider.GetRequiredService<FarewellService>();
// 5. Use the services
var greeting = greetingService.Greet("David");
var farewell = farewellService.SayGoodbye("David");
The preceding updated code demonstrates the how-to:
- Create a new
ServiceCollection
instance. - Register and configure services in the
ServiceCollection
:- The
IConsole
using the implementation factory overload, return aDefaultConsole
type with theIsEnabled
set to `true. - The
IGreetingService
is added with a corresponding implementation type ofDefaultGreetingService
type. - The
FarewellService
is added as a concrete type.
- The
- Build the
ServiceProvider
from theServiceCollection
. - Resolve the
IGreetingService
andFarewellService
services. - Use the resolved services to greet and say goodbye to a person named
David
.
If you update the IsEnabled
property of the DefaultConsole
to false
, the Greet
and SayGoodbye
methods omit writing to the resulting messages to console. A change like this, helps to demonstrate that the IConsole
service is injected into the IGreetingService
and FarewellService
services as a dependency that influences that apps behavior.
All of these services are registered as singletons, although for this sample, it works identically if they were registered as transient or scoped services.
Important
In this contrived example, the service lifetimes don't matter, but in a real-world application, you should carefully consider the lifetime of each service.
Run the sample app
To run the sample app, either press F5 in Visual Studio, Visual Studio Code, or run the dotnet run
command in the terminal. When the app completes, you should see the following output:
Hello, David!
Goodbye, David!
Service descriptors
The most commonly used APIs for adding services to the ServiceCollection
are lifetime-named generic extension methods, such as:
AddSingleton<TService>
AddTransient<TService>
AddScoped<TService>
These methods are convenience methods that create a ServiceDescriptor instance and add it to the ServiceCollection
. The ServiceDescriptor
is a simple class that describes a service with its service type, implementation type, and lifetime. It can also describe implementation factories and instances.
For each of the services that you registered in the ServiceCollection
, you could instead call the Add
method with a ServiceDescriptor
instance directly. Consider the following examples:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IConsole),
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
},
lifetime: ServiceLifetime.Singleton));
The preceding code is equivalent to how the IConsole
service was registered in the ServiceCollection
. The Add
method is used to add a ServiceDescriptor
instance that describes the IConsole
service. The static method ServiceDescriptor.Describe
delegates to various ServiceDescriptor
constructors. Consider the equivalent code for the IGreetingService
service:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IGreetingService),
implementationType: typeof(DefaultGreetingService),
lifetime: ServiceLifetime.Singleton));
The preceding code describes the IGreetingService
service with its service type, implementation type, and lifetime. Finally, consider the equivalent code for the FarewellService
service:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(FarewellService),
implementationType: typeof(FarewellService),
lifetime: ServiceLifetime.Singleton));
The preceding code describes the concrete FarewellService
type as both the service and implementation types. The service is registered as a singleton service.