Using Dependency Injection in .NET Console Apps

Are you tired of hearing the words "Dependency Injection" and instantly picturing complex web applications with fancy UIs? Well, my fellow developers, you're in for a treat, because in this article you'll see how to use Dependency Injection in console apps!

You might be thinking, "Console apps? Isn't that reserved for simple command-line utilities?" But, hold on. With Dependecy Injection, your humble console app can tap into the same power and flexibility that ASP.NET Core developers wield for their web applications. Sounds good, right? Let's dive in and see how we can turn even the most mundane console apps into flexible and testable applications.

In this article, I'll guide you through the process of setting up a basic console app with a ServiceCollection that'll make any ASP.NET Core developer feel right at home.

Installing NuGet packages

You'll need to install the Microsoft.Extensions.DependencyInjection package in your console app.

Using dotnet CLI

dotnet add package Microsoft.Extensions.DependencyInjection

Using Visual Studio

Use Visual Studio

Getting started in Program.cs

When you first create a console app, the boilerplate code will start you off with a Main method that writes "Hello, World!" to the the console. Inside this method, we're going to remove the Console.WriteLine() method and do our own applications setup and here. My preference is to create another class called "Application" where my apps actual logic will live. Then, Program.cs is simply a bootstrapping to launch the Application class with dependency injection.

using Microsoft.Extensions.DependencyInjection

namespace MyApp
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var services = CreateServices();

            Application app = services.GetRequiredService<Application>();
            app.MyLogic();
        }

        private static ServiceProvider CreateServices()
        {
            var serviceProvider = new ServiceCollection()
                .AddSingleton<Application>(new Application())
                .BuildServiceProvider();

            return serviceProvider;
        }
    }

    public class Application
    {
        public void MyLogic()
        {
            // Do something epic
        }
    }
}

Let's break down what's happening here. On line 9, we're setting a ServiceProvider variable which is the result of CreateServices(). In this method we register a single service in the collection. We're going to be handing off application flow to the Application class, so a singleton instance makes the most sense.

On line 11, we use the dependency injection container to resolve our singleton and then we begin the application logic. You might want to have a method in the Application class named "Start", "Initialize" or something similar.

Next steps

This structure gives us a flexible foundation to build upon. In the next sections, we'll look at how we can extend this and add some components you'll likely want to have in your very advanced console app.

Using EF Core

Usually when we're setting up EF Core, we have to retrieve the connection string from IConfiguration, add our database context to the DI container and possibly even run EF migrations when the application starts. Let's see how to do this.

Add NuGet packages

dotnet add package Microsoft.EntityFrameworkCore.Design

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

dotnet add package Microsoft.Configuration.UserSecrets

Changes to Program.cs

We've added some more using statements that allow us to work with EF Core and IConfiguration, don't forget those! There are two main changes to be aware.

  1. In CreateServices, an IConfiguration is created and for demonstration purposes I'm assuming my connection string is saved in my User Secrets file. The ServiceCollection now has the MyDbContext class registered to it. Again, for demonstration purposes I'm using the Sqlite provider.
  2. In the Main method, before starting my Application class we request an instance of MyDbContext to perform database migrations. This ensure that once our app's logic starts, the database will be in the correct state.
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection

namespace MyApp
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var services = CreateServices();

            MyDbContext context = services.GetRequiredService<MyDbContext>();
            context.Database.Migrate();

            Application app = services.GetRequiredService<Application>();
            app.MyLogic();
        }

        private static ServiceProvider CreateServices()
        {
            var configuration = new ConfigurationBuilder()
                .AddUserSecrets(Assembly.GetExecutingAssembly())
                .Build();

            string connectionString = configuration.GetConnectionString("MyApp");

            var serviceProvider = new ServiceCollection()
                .AddDbContext<MyDbContext>(options =>
                {
                    options.UseSqlite(connectionString);
                })
                .AddSingleton<Application>(new Application())
                .BuildServiceProvider();

            return serviceProvider;
        }
    }

    public class Application
    {
        public void MyLogic()
        {
            // Do something epic
        }
    }
}

Using ILogger for logging

Another common component you might want in your console app is modern logging to replace all those Console.WriteLine calls 🤣. For demonstration purposes we'll use the console logging provider.

🧠 There are many more logging providers available from third-parties. See this link for suggestions from Microsoft: https://learn.microsoft.com/en-us/dotnet/core/extensions/logging-providers#third-party-logging-providers

Add NuGet packages

dotnet add package Microsoft.Extensions.Logging.Console

Changes to Program.cs

On line 20, we're telling the dependency injection container that we're adding logging support. This comes from the using statement of Microsoft.Extensions.Logging. Then, I've modified the Application class so its constructor will receive an instance of ILogger which will come from our configured DI container. Take notice of how nothing needed to be changed on line 13 and 14. This is because our DI container will give us our configured singleton of Application, now with the ILogger<Application> included!

using System;
using Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.Logging;

namespace MyApp
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var services = CreateServices();

            Application app = services.GetRequiredService<Application>();
            app.MyLogic();
        }

        private static ServiceProvider CreateServices()
        {
            var serviceProvider = new ServiceCollection()
                .AddLogging(options =>
                {
                    options.ClearProviders();
                    options.AddConsole();
                })
                .AddSingleton<Application>(new Application())
                .BuildServiceProvider();

            return serviceProvider;
        }
    }

    public class Application
    {
        private readonly ILogger<Application> _logger;

        public Application(ILogger<Application> logger)
        {
            _logger = logger;
        }

        public void MyLogic()
        {
            _logger.LogInformation("Hello, World!");
        }
    }
}