Note: this article is published on 07/09/2024.
These will be a series of articles about Design Patterns. We start from MVC Pattern:
A: Introduction
.NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Inversion of Control is a principle in software engineering which transfers the control of objects or portions of a program to a container or framework.
This article will be an implementation of Dependency Injection for a Console app with a constructor injection and associated with a .NET Core built in container to handle the service lifetimes.
- A: Introduction
- B: Setup Environment
- C: Setup the Program
- D: Run and Result
B: Setup Environment:
Open a console App by Visual Studio 2022, Version 17.10.3, current on 06/30/2024
Project Name as ConsoleDI.Example
Framework: .NET 8.0 (Long Term Support)
Running:
In this example, the Microsoft.Extensions.Hosting NuGet package is required to build and run the app. Adding:
C: Setup the Program:
After the program is setting up, we will have these:
Now we go step by step according to the process of setting up a Dependency Injection,
- Setup Interface
- Make a service
- Register the service to a Container (.Net Core Default DI Container)
- Make a client
1. Setup Interfaces:
- IReportServiceLifetime --- Line 5
- A
Guid Id
property that represents the unique identifier of the service.
- A ServiceLifetime property that represents the service lifetime.
- IExampleSingletonService : IReportServiceLifetime --- Line 11
- IExampleScopedService : IReportServiceLifetime --- Line 16
- IExampleTransientService : IReportServiceLifetime --- Line 21
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleDI.Example;
public interface IReportServiceLifetime
{
Guid Id { get; }
ServiceLifetime Lifetime { get; }
}
public interface IExampleSingletonService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}
public interface IExampleScopedService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}
public interface IExampleSingletonService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}
Note:
For convenience, I combine the four saperated pages for interfaces in one as above:
2. Make Services:
Implementations for the interfaces: Initializing their Id
property with the result of Guid.NewGuid().
- internal sealed class ExampleSingletonService : IExampleSingletonService --- Line 3
- internal sealed class ExampleScopedService : IExampleScopedService --- Line 8
- internal sealed class ExampleTransientService : IExampleTransientService --- Line 13
Each implementation is defined as internal sealed
and implements its corresponding interface. For example, ExampleSingletonService
implements IExampleSingletonService
.
namespace ConsoleDI.Example;
internal sealed class ExampleSingletonService : IExampleSingletonService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
internal sealed class ExampleScopedService : IExampleScopedService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
internal sealed class ExampleTransientService : IExampleTransientService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}
Note:
For convenience, I combine the tree services in one page as above:
3. Register Services for DI (.NET Core default DI container): in program.cs
- builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>(); --- Line 9
- builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>(); --- Line 10
- builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>(); --- Line 11
- builder.Services.AddTransient<ServiceLifetimeReporter>(); --- Line 12
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;
//From: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();
using IHost host = builder.Build();
ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");
await host.RunAsync();
static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
using IServiceScope serviceScope = hostProvider.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine("...");
logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine();
}
4. Make a Client: in ServiceLifetimeReporter.cs
Add a service that requires DI
- internal sealed class ServiceLifetimeReporter(
IExampleTransientService transientService,
IExampleScopedService scopedService,
IExampleSingletonService singletonService) --- Line 3
The ServiceLifetimeReporter
defines a constructor that requires each of the aforementioned service interfaces, that is, IExampleTransientService
, IExampleScopedService
, and IExampleSingletonService
. The object exposes a single method that allows the consumer to report on the service with a given lifetimeDetails
parameter. When invoked, the ReportServiceLifetimeDetails
method logs each service's unique identifier with the service lifetime message. The log messages help to visualize the service lifetime.
namespace ConsoleDI.Example;
internal sealed class ServiceLifetimeReporter(
IExampleTransientService transientService,
IExampleScopedService scopedService,
IExampleSingletonService singletonService)
{
public void ReportServiceLifetimeDetails(string lifetimeDetails)
{
Console.WriteLine(lifetimeDetails);
LogService(transientService, "Always different");
LogService(scopedService, "Changes only with lifetime");
LogService(singletonService, "Always the same");
}
private static void LogService<T>(T service, string message)
where T : IReportServiceLifetime =>
Console.WriteLine(
$" {typeof(T).Name}: {service.Id} ({message})");
}
Note:
Although this class is named as ServiceLifetimeReporter, it is a service in the main program, but it is a client to consume the service provided by the interfaces and their implenentations.
Copying all of these code above, it will be runable.
C: Run and Result
From the app output, you can see that:
- Transient services are always different, a new instance is created with every retrieval of the service (each has a different color underlined).
- Scoped services change only with a new scope, but are the same instance within a scope (Green and Light Blue in two scopes respectively).
- Singleton services are always the same (red), a new instance is only created once.
These results can be demoed by the following graph:
References: