DEV Community

Emanuel Gustafzon
Emanuel Gustafzon

Posted on

Dependency Injection Dotnet

Welcome to the last post of this series and congrats for getting this far. You should now have a solid understanding of Dependecy Injection so let's implement it in dotnet.

Lifetimes

Dotnet support different lifetimes, Transient, Scoped and Singleton.

You would tipically use Scoped in WebApps and Apis where the scoped lifetime is the lifetime of a HTTP Context. If you work in Maui or WPF etc you need to explicity create the scope and that adds extra complexity so normally you would use Transient or Singleton in those types of applications.

SetUp

You do the setup and configuration in Program.cs.

If you work in Maui or ASP.NET Core, you will see that Dependecy injection is already setup but in this example we use a Console App so we need to set it up ourselves.

Start off by downloading the following packages.

Microsoft.Extensions.Hosting
Microsoft.Extensions.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

Create the default builder and configure your services

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        // Add Your Services here
    }).Build();
Enter fullscreen mode Exit fullscreen mode

Add collection of services

So as you can see below you can add a collection of services with a defined lifetime.

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<MySingleTonService>();
        services.AddScoped<MyScopedService>();
        services.AddTransient<MyTransientService>();
    }).Build();
Enter fullscreen mode Exit fullscreen mode

Add interfaces

You can also add a interface to the registered service which is also best practice.

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddSingleton<IMyService, MyService>();
    }).Build();
Enter fullscreen mode Exit fullscreen mode

IServiceProvider

The collection of services we just configured implements the interface IServiceprovider.

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        // My Collection of registered services
    }).Build();

IServiceProvider serviceProvider = host.Services;
Enter fullscreen mode Exit fullscreen mode

Put it all together

Create the Services and interfaces

In this example we have a notification service that is dependent on an email service.

public interface IEmailService
{
    public void SendEmail(string email, string message);
}

public class EmailService : IEmailService
{
    public void SendEmail(string email, string message)
    {
        Console.WriteLine($"Sending {message} to {email}");
    }
}
public interface INotificationService
{
    public void NotifyUser(User user, string message);
}

public class NotficationService : INotificationService
{
    IEmailService _emailService;

    public NotficationService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void NotifyUser(User user, string message)
    {
        _emailService.SendEmail(user.Email, message);
    }
}
public class User
{
    public string Name { get; set; } = null!;
    public string Email { get; set; } = null!;
}

Enter fullscreen mode Exit fullscreen mode

Register the services

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddTransient<IEmailService, EmailService>();
        services.AddTransient<INotificationService, NotficationService>();
    }).Build();
Enter fullscreen mode Exit fullscreen mode

Access the Notification service

var notify =  host.Services.GetRequiredService<INotificationService>();
User user = new User { Name = "Emanuel", Email = "Emanuel@domain.com" };
notify.NotifyUser(user, "Hello There!");

// output: Sending Hello There! to Emanuel@domain.com
Enter fullscreen mode Exit fullscreen mode

Use IServiceProvider to access services

You can also access services through IServiceProvider. Let us rewrite the Notification Service.

public class NotficationService : INotificationService
{
    IServiceProvider _serviceProvider;

    public NotficationService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void NotifyUser(User user, string message)
    {
        var emailService = _serviceProvider.GetRequiredService<IEmailService>();
        emailService.SendEmail(user.Email, message);
    }
}
Enter fullscreen mode Exit fullscreen mode

This will work in the exact same way and we have more control over what services we want to access.

Different Lifetimes in action.

So in a Console App we need to create the scope explicity so let's do that.

For demostration purpose I will create a model that contains a unique ID and we will see how i singleton returns the same instance throughout the hole program and therefore the same ID. Scoped will return the same instance in the same scope and a transient will return a new instance everytime.

var host = Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddScoped<UniqueIDSingelton>();
        services.AddScoped<UniqueIDScoped>();
        services.AddTransient<UniqueIDTransient>();
    }).Build();
public class UniqueIDSingelton
{
    public string ID = Guid.NewGuid().ToString();
}
public class UniqueIDScoped
{
    public string ID = Guid.NewGuid().ToString();
}
public class UniqueIDTransient
{
    public string ID = Guid.NewGuid().ToString();
}
Enter fullscreen mode Exit fullscreen mode

Create two scopes.

var scope1 = host.Services.CreateScope();
var scope2 = host.Services.CreateScope();
Enter fullscreen mode Exit fullscreen mode

Now let us see how a serviice with scoped lifetime behave.

var scope1 = host.Services.CreateScope();

var UniqueId1Scope1 = scope1.ServiceProvider.GetRequiredService<UniqueIDScoped>();
var UniqueId2Scope1 = scope1.ServiceProvider.GetRequiredService<UniqueIDScoped>();

Console.WriteLine(UniqueId1Scope1.ID == UniqueId2Scope1.ID); // true

var scope2 = host.Services.CreateScope();
var UniqueIdScope2 = scope2.ServiceProvider.GetRequiredService<UniqueIDScoped>();


Console.WriteLine(UniqueId1Scope1.ID == UniqueIdScope2.ID); // false
Enter fullscreen mode Exit fullscreen mode

The instance will be the same in the same scope as you see.

let us move over to Singelton. You will see that even though I get the service in two different scopes the instance and therfore the ID is the same.

var scope1 = host.Services.CreateScope();
var singeltonScope1 = scope1.ServiceProvider.GetRequiredService<UniqueIDSingelton>();

var scope2 = host.Services.CreateScope();
var singeltonScope2 = scope1.ServiceProvider.GetRequiredService<UniqueIDSingelton>();

Console.WriteLine(singeltonScope1.ID == singeltonScope2.ID); // true
Enter fullscreen mode Exit fullscreen mode

Last let's see Transient in action and how it gives a new instance everytime. Below we call the service twice in the same scope but the instance and therefore the ID is different.

var scope1 = host.Services.CreateScope();
var transient1 = scope1.ServiceProvider.GetRequiredService<UniqueIDTransient>();
var transient2 = scope1.ServiceProvider.GetRequiredService<UniqueIDTransient>();

Console.WriteLine(transient1.ID == transient2.ID); // false
Enter fullscreen mode Exit fullscreen mode

Alright there you have Dependency Injection in Dotnet and as I told you in an ASP.NET Core Application you do not need to explicity create the scope because the scope is already configured to be the scope of a HTTP request and in other types of applications like Maui etc, you would rather use Transient and Singelton.

Hope you found it useful and happy coding!

Top comments (0)