5

I created a new .NET Core console app and installed the following packages

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Microsoft.Extensions.Configuration.Json
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Options
  • Microsoft.Extensions.Options.ConfigurationExtensions

I created a appsettings.json file for the configuration

{
  "app": {
    "foo": "bar" 
  }
}

and I want to map those values to a class

internal class AppOptions
{
    public string Foo { get; set; }
}

I also want to validate the options during configuration so I added a validating class

internal class AppOptionsValidator : IValidateOptions<AppOptions>
{
    public ValidateOptionsResult Validate(string name, AppOptions options)
    {
        IList<string> validationFailures = new List<string>();

        if (string.IsNullOrEmpty(options.Foo))
            validationFailures.Add("Foo is required.");

        return validationFailures.Any()
        ? ValidateOptionsResult.Fail(validationFailures)
        : ValidateOptionsResult.Success;
    }
}

I want to setup the DI container and created a testing scenario

    static void Main(string[] args)
    {
        ConfigureServices();

        Console.ReadLine();
    }

    private static void ConfigureServices()
    {
        IServiceCollection serviceCollection = new ServiceCollection();
        IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

        // Setup configuration service

        IConfiguration configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", true, true)
            .AddEnvironmentVariables()
            .Build();

        serviceCollection.AddSingleton(configuration);

        // Setup options

        IConfiguration configurationFromDI = serviceProvider.GetService<IConfiguration>(); // This is just for testing purposes

        IConfigurationSection myConfigurationSection = configurationFromDI.GetSection("app");

        serviceCollection.AddSingleton<IValidateOptions<AppOptions>, AppOptionsValidator>();
        serviceCollection.Configure<AppOptions>(myConfigurationSection);

        // Try to read the current options

        IOptions<AppOptions> appOptions = serviceProvider.GetService<IOptions<AppOptions>>();

        Console.WriteLine(appOptions.Value.Foo);
    }

Unfortunately the variable configurationFromDI is null. So the variable configuration wasn't added to the DI container.

How do I setup the Dependency Injection for console applications correctly?

  • Put `serviceCollection.BuildServiceProvider();` after your add – TheGeneral Dec 10 '20 at 08:53
  • 4
    There's no need to write all of this code though. Since you use so many extensions already, it's better (and easier) to use HostBuilder, the same way an ASP.NET Core application does and use its `ConfigureServices`, `ConfigureAppConfiguration` methods – Panagiotis Kanavos Dec 10 '20 at 08:55
  • Does this answer your question? [Startup.cs in a self-hosted .NET Core Console Application](https://stackoverflow.com/questions/41407221/startup-cs-in-a-self-hosted-net-core-console-application) – Michael Freidgeim May 08 '22 at 07:09

2 Answers2

6

The call to BuildServiceProvider should be made after all services are registered.

There's no need to write all of this code though. Since you use so many extensions already, it's better (and easier) to use the generic Host, the same way an ASP.NET Core application does and use its ConfigureServices, ConfigureAppConfiguration methods:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureHostConfiguration(configuration =>
            {
                configuration....;
            });
            .ConfigureServices((hostContext, services) =>
            {
                var myConfigurationSection = configuration.GetSection("app");

                services.AddSingleton<IValidateOptions<AppOptions>, AppOptionsValidator>();
                services.Configure<AppOptions>(myConfigurationSection);

            });
}

Configuration is available through the HostBuilderContext.Configuration property.

CreateDefaultBuilder sets the current folder, configures environment variables and the use of appsettings.json files so there's no need to add them explicitly.

Appsettings.json copy settings

In a web app template, appsettings.json files are added automatically with the Build Action property set to Content and the Copy to Output action to Copy if Newer.

There are no such files in a Console app. When a new appsettings.json file is added by hand, its Build Action is None and Copy to Never. When the application is debugged the current directory is bin\Debug. With the default settings, appsettings.json won't be copied to bin/Debug

Build Action will have to change to Content and Copy should be set to Copy if Newer or Copy Always.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • thanks! :) I tried out your code example https://pastebin.com/bDvh9vuK but unfortunately it's not able to read from the appsettings.json file. I always get options validation failures. Do you know what's missing? What needs to be in `ConfigureHostConfiguration`? –  Dec 10 '20 at 10:15
  • Nothing's missing. I use this in a dozen CLI tools, so it's not just code copied from the docs. Is `appsettings.json` copied to the output folder? When you add a new JSON file its `Build Action` is `None` and the `Copy` setting is set to `Never`. When you debug your application the current directory is `bin/Debug` so your app won't see any `json` files there. You need to set `Copy` to `Always` or `Copy if newer`. You need to set the `Build Action` to `Content` to have the file included during publishing or packaging – Panagiotis Kanavos Dec 10 '20 at 10:20
  • ah didn't know that :) Since there is nothing to do in `ConfigureAppConfiguration` and `ConfigureHostConfiguration` I removed them. The code still seems to work fine. Would you mind having a look at the updated version? https://pastebin.com/VYsfSbEe –  Dec 10 '20 at 10:33
1

DI in Console project

You can setup DI in any executable .net-core app and configure services in a Startup class (just like web projects) by extending IHostBuilder:

public static class HostBuilderExtensions
{
    private const string ConfigureServicesMethodName = "ConfigureServices";

    public static IHostBuilder UseStartup<TStartup>(
        this IHostBuilder hostBuilder) where TStartup : class
    {
        hostBuilder.ConfigureServices((ctx, serviceCollection) =>
        {
            var cfgServicesMethod = typeof(TStartup).GetMethod(
                ConfigureServicesMethodName, new Type[] { typeof(IServiceCollection) });

            var hasConfigCtor = typeof(TStartup).GetConstructor(
                new Type[] { typeof(IConfiguration) }) != null;

            var startUpObj = hasConfigCtor ?
                (TStartup)Activator.CreateInstance(typeof(TStartup), ctx.Configuration) :
                (TStartup)Activator.CreateInstance(typeof(TStartup), null);

            cfgServicesMethod?.Invoke(startUpObj, new object[] { serviceCollection });
        });

        return hostBuilder;
    }
}

Now, you have an extension method UseStartup<>() that can be called in Program.cs (the last line):

public class Program
{
    public static void Main(string[] args)
    {
        // for console app
        CreateHostBuilder(args).Build().Run(); 
      
        // For winforms app (commented)
        // Application.SetHighDpiMode(HighDpiMode.SystemAware);
        // Application.EnableVisualStyles();
        // Application.SetCompatibleTextRenderingDefault(false);
        // var host = CreateHostBuilder(args).Build();
        // Application.Run(host.Services.GetRequiredService<MainForm>());
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            
            .ConfigureServices((hostContext, services) =>
            {
                config.AddJsonFile("appsettings.json", optional: false);
                config.AddEnvironmentVariables();
                // any other configurations
            })
            .UseStartup<MyStartup>();
}

Finally, Add your own Startup class (here, MyStartup.cs), and inject IConfiguration from constructor:

public class MyStartup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // services.AddBlahBlahBlah()
    }
}

P.S: you get null because you call .BuildServiceProvider before registering IConfiguration

Mapping appsettings.json

For mapping appsettings.json values to a type, define an empty interface like IConfigSection (just for generic constraints reason):

public interface IConfigSection
{
}

Then, extend IConfiguration interface like follow:

public static class ConfigurationExtensions
{
    public static TConfig GetConfigSection<TConfig>(this IConfiguration configuration) where TConfig : IConfigSection, new()
    {
        var instance = new TConfig();
        var typeName = typeof(TConfig).Name;
        configuration.GetSection(typeName).Bind(instance);

        return instance;
    }
}

Extension methdod GetConfigSection<>() do the mapping for you. Just define your config classes that implement IConfigSection:

public class AppConfigSection : IConfigSection
{
    public bool IsLocal { get; set; }
    public bool UseSqliteForLocal { get; set; }
    public bool UseSqliteForServer { get; set; }
}

Below is how your appsettings.json should look like (class name and property names should match):

{

.......

"AppConfigSection": {
    "IsLocal": false,
    "UseSqliteForServer": false,
    "UseSqliteForLocal": false
  },

.....

}

And finally, retrieve your settings and map them to your ConfigSections as follow:

// configuration is the injected IConfiguration
AppConfigSection appConfig = configuration.GetConfigSection<AppConfigSection>();
Dharman
  • 30,962
  • 25
  • 85
  • 135
Efe
  • 800
  • 10
  • 32
  • I think where you have (inside Program.cs): .ConfigureServices((hostContext, services) => you meant: .ConfigureHostConfiguration((config) => – Robb Sadler Aug 19 '22 at 18:20
  • @RobbSadler depends on framework, .net-core or dotnet – Efe Aug 20 '22 at 09:21