Search code examples
c#.net-coredependency-injectionconsole-application

Accessing DI elements like IConfiguration from within nested class in a Core console app


I have browsed many tutorials and watch several videos regarding using DI in .NET Core console applications. However, they all seem to fall a little short of what I'm looking for. In a large console app we might have multiple classes being called/referenced and each of these classes may have a need to access the logger or the config settings. All of the tutorials and explanations I have come across have only shown one level deep and passing the logging and configuration DI instances to the first level.

What if Class 1 calls Class 2, which then calls class 3. Say class 3 needs to get a configuration value or class 2 needs to write some informational or debug data to the log. How does one access ILogger and IConfiguration using DI in classes 2 and 3?

I have created a very simple example here based on the video tutorial of Tim Corey, ".NET Core Console App with Dependency Injection, Logging, and Settings", where he sets up config and serilog for a console app. but again, he only goes as deep as the initial worker class.

The app consists of 3 main classes, Program.cs (used to set up configurations), Runner.cs (the worker service class), and SubClass.cs (the sub class called from Runner.cs). My code works for logging and retrieving settings in the worker service class (Runner.cs), but when I try to access the logger or the config settings from the SubClass, I run into issues.

Program.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;

namespace DiConsoleExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder();
            BuildConfig(builder);

            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(builder.Build())
                .Enrich.FromLogContext()
                .WriteTo.Console()
                .CreateLogger();

            Log.Logger.Information("Application Starting");

            var host = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddTransient<IRunner, Runner>();
                })
                .UseSerilog()
                .Build();

            var svc = ActivatorUtilities.CreateInstance<Runner>(host.Services);
            svc.Run();
        }

        static void BuildConfig(IConfigurationBuilder builder)
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        }

    }
}

Runner.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace DiConsoleExample
{
    public interface IRunner
    {
        void Run();
    }

    public class Runner : IRunner
    {
        private readonly ILogger<Runner> _log;
        private readonly IConfiguration _config;

        public Runner(ILogger<Runner> log, IConfiguration config)
        {
            _log = log;
            _config = config;
        }


        public void Run()
        {
            _log.LogInformation("Value for 'MyRunnerSetting': {settingValue}", _config.GetValue<string>("MyRunnerSetting"));

            var sub = new SubClass();
            sub.GetMySetting();
        }
    }
}

SubClass.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace DiConsoleExample
{
    public class SubClass
    {
        private readonly ILogger<Runner> _log;
        private readonly IConfiguration _config;

        public SubClass(ILogger<Runner> log, IConfiguration config)
        {
            _log = log;
            _config = config;
        }

        public void GetMySetting()
        {
            _log.LogInformation("Value for 'MySubClassSetting': {settingValue}", _config.GetValue<string>("MySubClassSetting"));
        }
    }
}

appsettings.json

{
  "MyRunnerSetting": "Runner123",
  "MySubClassSetting":  "SubClass234"
}

The error I'm getting is that when I create a new SubClass() it can't use the DI injected Log and Config services: There is no argument given that corresponds to the required parameter 'log' of 'SubClass.SubClass(ILogger<Runner>, IConfiguration)

Again, this works fine if the call to SubClass is not made in Runner.cs, but I need to know how to access the Logger and Configurations through DI. I know I can put the build/configure statements for each in each class, but that seems to defeat the purpose of DI and duplicates code.

Oh, and I am using .NET 8 in VS 2022.


EDIT. I should not have used "SubClass" in my explanation as it does not inherit from anything. I should have used "NestedClass" (my bad)

My question since my Runner.cs might new up multiple nested classes in a large app, how do I get the ILogger and IConfiguration into those nested classes?

In the .net framework, I could put log4net in any nested class to start logging or call ConfigurationManager in any class to get a setting. In Core ASP.NET I can use a constructor with ILogger and IConfiguration in every controller, but I can't figure out how to get to or reference ILogger and IConfiguration in any nested classes beyond the Runner.cs service.


Solution

  • Throw away all the things you know about .NET Framework regarding logging and configuration, because they're irrelevant in the .NET (Core) world. You never manually new classes up, the DI framework provides them for you. And you don't bind directly to IConfiguration, you use the options pattern. So your classes would look something like this:

    public record SubClassOptions(
        string MySubClassSetting);
    
    public class Runner
    {
        private ILogger<Runner> _logger;
        private ISubClass _subClass;
    
        public Runner(ILogger<Runner> logger, ISubClass subClass)
        {
            _logger = logger;
            _subClass = subClass;
        }
    
        public void Run()
        {
            _subClass.GetMySetting();
        }
    }
    
    public interface ISubClass
    {
        void GetMySetting();
    }
    
    public class SubClass : ISubClass
    {
        private readonly ILogger<Runner> _logger;
        private readonly SubClassOptions _options;
    
        public SubClass(ILogger<Runner> log, SubClassOptions config)
        {
            _log = log;
            _config = config;
        }
    
        public void GetMySetting()
        {
            _log.LogInformation(
                "Value for 'MySubClassSetting': {SettingValue}",
                _config.MySubClassSetting);
        }
    }
    

    and your service registrations:

    services.AddSingleton<SubClassOptions>(
        configuration.GetRequiredSection("SubClassSection").Get<SubClassOptions>());
    services.AddScoped<ISubClass, SubClass>();
    services.AddScoped<Runner>();