Tutorial: Use dependency injection in .NET

This tutorial shows how to use dependency injection (DI) in .NET. With Microsoft Extensions, DI is managed by adding services and configuring them in an IServiceCollection. The IHost interface exposes the IServiceProvider instance, which acts as a container of all the registered services.

In this tutorial, you learn how to:

  • Create a .NET console app that uses dependency injection
  • Build and configure a Generic Host
  • Write several interfaces and corresponding implementations
  • Use service lifetime and scoping for DI

Prerequisites

  • .NET Core 3.1 SDK or later.
  • Familiarity with creating new .NET applications and installing NuGet packages.

Create a new console application

Using either the dotnet new command or an IDE new project wizard, create a new .NET console application named ConsoleDI.Example. Add the Microsoft.Extensions.Hosting NuGet package to the project.

Your new console app project file should resemble the following:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

Important

In this example, the Microsoft.Extensions.Hosting NuGet package is required to build and run the app. Some metapackages might contain the Microsoft.Extensions.Hosting package, in which case an explicit package reference isn't required.

Add interfaces

In this sample app, you'll learn how dependency injection handles service lifetime. You'll create several interfaces that represent different service lifetimes. Add the following interfaces to the project root directory:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

The IReportServiceLifetime interface defines:

  • A Guid Id property that represents the unique identifier of the service.
  • A ServiceLifetime property that represents the service lifetime.

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

All of the subinterfaces of IReportServiceLifetime explicitly implement the IReportServiceLifetime.Lifetime with a default. For example, IExampleTransientService explicitly implements IReportServiceLifetime.Lifetime with the ServiceLifetime.Transient value.

Add default implementations

The example implementations all initialize their Id property with the result of Guid.NewGuid(). Add the following default implementation classes for the various services to the project root directory:

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

Each implementation is defined as internal sealed and implements its corresponding interface. For example, ExampleSingletonService implements IExampleSingletonService.

Add a service that requires DI

Add the following service lifetime reporter class, which acts as a service to the console app:

ServiceLifetimeReporter.cs

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})");
}

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.

Register services for DI

Update Program.cs with the following code:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

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();
}

Each services.Add{LIFETIME}<{SERVICE}> extension method adds (and potentially configures) services. We recommend that apps follow this convention. Don't place extension methods in the Microsoft.Extensions.DependencyInjection namespace unless you're authoring an official Microsoft package. Extension methods that are defined within the Microsoft.Extensions.DependencyInjection namespace:

  • Are displayed in IntelliSense without requiring additional using blocks.
  • Reduce the number of required using statements in the Program or Startup classes where these extension methods are typically called.

The app:

Conclusion

In this sample app, you created several interfaces and corresponding implementations. Each of these services is uniquely identified and paired with a ServiceLifetime. The sample app demonstrates registering service implementations against an interface, and how to register pure classes without backing interfaces. The sample app then demonstrates how dependencies defined as constructor parameters are resolved at run time.

When you run the app, it displays output similar to the following:

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

From the app output, you can see that:

  • Transient services are always different, a new instance is created with every retrieval of the service.
  • Scoped services change only with a new scope, but are the same instance within a scope.
  • Singleton services are always the same, a new instance is only created once.

See also