Design Pattern (5-3), Dependency Injection, Console Demo

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, 

  1. Setup Interface
  2. Make a service
  3. Register the service to a Container (.Net Core Default DI Container)
  4. Make a client

1. Setup Interfaces:

  • IReportServiceLifetime --- Line 5
    • Guid Id property that represents the unique identifier of the service.
    • 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, IExampleTransientServiceIExampleScopedService, 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: