简体   繁体   中英

How to setup the DI container in a .NET Core console app?

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?

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 .

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()
    }
}

PS: 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>();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM