Reputation:
I created a new .NET Core console app and installed the following packages
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?
Upvotes: 6
Views: 7800
Reputation: 880
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>();
Upvotes: 2
Reputation: 131473
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
.
Upvotes: 7