Why does a console application with IHostedService work with scoped dependencies when there is no explicit scope?

Viewed 64

I was creating a console application that is reusing a library with some dependencies registered as AddScoped. I was afraid my app would fail if I won't create a scope myself, since I have already seen some Stack Overflow questions regarding scope management in console applications.

However, I was surprised to see that my app worked just fine!

Now I'm puzzled as to how does it work and which component is resolving the scoped dependencies automagically for me? Is CreateDefaultBuilder doing something special? Or is it RunConsoleAsync?

I have created a simple console application as a proof of concept (the code is for .net6 style of Program.cs). You might expect DI to complain about being unable to resolve IMyInterface dependency because I have registered it with AddScoped and MyImplementation2 depends on it (and, in turn, the final host depends on IMyInterface2 singleton) and I am not creating a scope anywhere.

At least, in a web app it would fail screaming at me that the singleton MyImplementation2 was unable to resolve a scoped implementation of IMyInterface.

However, the console app succeeds and prints out the text from both Hello() methods.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

await Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostingContext, services) =>
    {
        services.AddSingleton<IMyInterface2, MyImplementation2>()
                .AddScoped<IMyInterface, MyImplementation>()
            .AddHostedService<MyConsoleHostedService>();
    }).RunConsoleAsync();

internal interface IMyInterface
{
    void Hello();
}

internal class MyImplementation : IMyInterface
{
    public void Hello()
    {
        Console.WriteLine("Hello, MyImplementation");
    }
}

internal interface IMyInterface2
{
    void Hello();
}

internal class MyImplementation2 : IMyInterface2
{
    IMyInterface myScoped;

    public MyImplementation2(IMyInterface my)
    {
        myScoped = my;
    }

    public void Hello()
    {
        myScoped.Hello();
        Console.WriteLine("Hello, MyImplementation2");
    }
}

internal sealed class MyConsoleHostedService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;
    private readonly IMyInterface2 myDependency;

    public MyConsoleHostedService(IHostApplicationLifetime appLifetime,
        IMyInterface2 myDependency)
    {   
        this.appLifetime = appLifetime;
        this.myDependency = myDependency;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        appLifetime.ApplicationStarted.Register(() =>
        {
            Task.Run(() =>
            {
                myDependency.Hello();
                appLifetime.StopApplication();
            });
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

So, why does it work? Where is the scope and how is the scoped IMyInterface resolved for the singleton MyImplementation2?

0 Answers
Related