ASP.NET Core UseSetting from integration test - c#

I have an integration tests project that uses .UseSetting() in the test class, as follows:
public AccessTokenRetrieval() : base(nameof(AccessTokenRetrieval))
{
var connectionString = GetConnectionString();
var dbSettings = new DbSettings(connectionString);
_userGroupRepository = new UserGroupRepository(dbSettings);
_roleRepository = new RoleRepository(dbSettings);
_userRepository = new UserRepository(dbSettings);
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
.UseEnvironment("IntegrationTest")
.UseSetting("IntegrationTestConnString", dbSettings.IdentityServerConnectionString));
_handler = _server.CreateHandler();
_client = _server.CreateClient();
}
I would now like to retrieve that setting in the Startup.cs of my actual project. I attempted to do so using:
public void ConfigureIntegrationTestServices(IServiceCollection services)
{
var connectionString = Configuration.GetValue<string>("IntegrationTestConnString");
BuildIdentityServerTests(services, connectionString);
AddCoreServices(services, connectionString);
}
but that seems to return null.
What is the proper way to retrieve this setting?

You could just inject the IConfigurationRoot where you could just use a local config file in the test project. You'd lose the default environment-specific config overrides that are the default in the new project template, but I personally don't use those for configuration management anyway (preferring non-committed appsetting.local.json instead which can be unique to developers and to the CI server).
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true)
.AddInMemoryCollection(new Dictionary<string, string>
{
{"myConfig:setting1", "value1"},
{"myConfig:setting2", "value2"}
})
.Build();
var server = new TestServer(new WebHostBuilder()
.UseUrls("http://localhost:5001")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.ConfigureServices(services => services.AddSingleton(configuration)));
And for Startup:
public class Startup
{
public Startup(IConfigurationRoot configuration)
{
this.Configuration = configuration;
}
...
}
Edit:
While this does work, it also seems to break IOptionsSnapshot configuration injections from recognizing config file changes at run-time. I'm going to leave this answer, but will also keep digging for better way without injecting a custom service just to support test/fixture-specific config overrides.

Per-test setting
To pass a setting into a particular TestHost instance, you could use ConfigureServices call and override default setting registration.
First of all, register your default DbSettings instance in Startup.ConfigureServices method. It's mandatory to use TryAll call:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton(new DbSettings(...));
}
}
Register a mocked instance in WebHostBuilder.ConfigureServices call:
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton(new DbSettings(...));
}
);
When you try to resolve a DbSettings in the application code, you will get a mocked instance. Because WebHostBuilder.ConfigureServices is executed first and TryAdd call prevents to register a default instance.
This hint allows to replace any DI dependency you registered.
Global setting
To set an invariant (accross all tests) setting, set an process-scoped environment variable instead of UseSetting call:
Environment.SetEnvironmentVariable("foo", "bar");
var _server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>()
);
Then read it:
public void ConfigureServices(IServiceCollection services)
{
string setting1 = Configuration["foo"];
string setting2 = Environment.GetEnvironmentVariable("foo");
}
You need to add environment variable provider to read variables from Configuration:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
//this config provider is mandatory to read env vars from Configuration
.AddEnvironmentVariables();
Configuration = builder.Build();
}

Related

Serilog custom sink with dependency Injection

I know there are several questions on this topic but haven't found anything working for me.
My problem seems to be that there is some kind of circular reference because I keep hitting the code in UseSerilog() over and over. I have tried to inject several different services but all get the same problem. In program.cs I have:
public static void Main()
{
var host = new HostBuilder()
.ConfigureAppConfiguration(config =>
{
var environmentName = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT");
if (environmentName.IsDevelopment())
{
config.AddJsonFile($"local.settings.json", optional: true, reloadOnChange: false);
}
config.AddEnvironmentVariables();
})
.UseSerilog((context, services, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.WriteTo.WintSink(services.GetRequiredService<IMyRequiredService>());
})
.ConfigureServices(ConfigureServices)
.Build();
host.Run();
}
Update 1:
I have managed to get it working if I use the Serilog.ILogger interface instead of Microsoft.Extensions.Logging.ILogger (not what I wanted) in combination with:
services.AddSingleton<Serilog.ILogger>(sp =>
{
var service = sp.GetRequiredService<IMyRequiredService>();
return new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.WintSink(service)
.CreateLogger();
});
services.AddLogging(configure => configure.ClearProviders().AddSerilog());

How can I use a strongly-typed config within the Startup class

I have a strongly-typed config working but I am struggling with using the strongly-typed class within the "class Startup"
Here is my code in startup:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Allow sites to hit the API
services.AddCors();
// Add Mvc Routes
services.AddMvc(option => option.EnableEndpointRouting = false);
// Add custom configuration
services.Configure<Config>(Configuration);
I'd like to use the strongly-typed class in another function in that file, but I'm not sure how I can do that...
This is how it works in other classes:
readonly Config Config = null;
public EmailController(IOptions<Config> config)
{
Config = config.Value;
}
But if I try to add the same code to my "class Startup" then I get this exception
System.ArgumentNullException: 'Value cannot be null. (Parameter 'config')'
on the line
services.Configure<Config>(Configuration);
I think you are asking how can you use your config inside the startup class before you are allowed to inject it into a constructor?
You can use Get or Bind methods on IConfiguration
// Add custom configuration
services.Configure<Config>(Configuration);
//Get the config object for use here
var config = Configuration.Get<Config>();

How to read configuration settings before initializing a Host in ASP .NET Core?

Before initializing the application Host I need to read some settings from the application's configuration to setup some other things.
In ASP .NET Core 2.x to read settings before initializing the application Host I used to do the following:
public static void Main(string[] args)
{
//...
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddCommandLine(args)
.AddJsonFile("appsettings.json")
.Build();
//Do something useful with the configuration...
var host = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.UseConfiguration(configuration)
.Build();
//...
}
In ASP .NET Core 3.x WebHost has been deprecated in favor of .NET Generic Host.
.NET Generic Host has only .ConfigureHostConfiguration() and .ConfigureAppConfiguration() that do not take a built configuration as parameter, but instead accept only a delegate used to setup the configuration.
For HTTP workloads you can still use the method .UseConfiguration() has it is exposed by IWebHostBuilder and do essentially the same as before:
public static void Main(string[] args)
{
//...
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddCommandLine(args)
.AddJsonFile("appsettings.json")
.Build();
//Do something useful with the configuration...
var host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseConfiguration(configuration);
})
.Build();
//...
}
But this only works for HTTP workloads and not for Worker Services.
To get the configuration before setting up the Host I've come up with the following approach:
public static void Main(string[] args)
{
//...
var configuration = ConfigureConfigurationBuilder(args)
.Build();
//Do something useful with the configuration...
var host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder => ConfigureConfigurationBuilder(args, builder))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build();
//...
}
public static IConfigurationBuilder ConfigureConfigurationBuilder(string[] args, IConfigurationBuilder configurationBuilder = null)
{
configurationBuilder ??= new ConfigurationBuilder();
configurationBuilder
.AddEnvironmentVariables()
.AddCommandLine(args)
.AddJsonFile("appsettings.json");
return configurationBuilder;
}
Essentially I've wrote a method that setups a ConfigurationBuilder and returns it, so that I can reuse the same configuration. This works in practice, but I build the same configuration twice.
Is there a simpler/more correct way (that works for HTTP workloads and non-HTTP workloads) to build and reuse the configuration before setting up the Host ?
You can clear the default sources added by CreateDefaultBuilder then add a pre-built IConfiguration with the AddConfiguration extension method.
public static void Main(string[] args)
{
//...
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddCommandLine(args)
.AddJsonFile("appsettings.json")
.Build();
//Do something useful with the configuration...
var host = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
builder.Sources.Clear();
builder.AddConfiguration(configuration);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build();
//...
}
The answer by UncleDave is certainly the best and most correct way to do this, but if you want to use the default configuration without recreating the logic yourself, it is not easy to get access to the IConfiguration and the IWebHostBuilder in the same place.
In order to do this, you can take advantage of the fact that the concrete Configuration is built before other services such as the web host are built. You can use ConfigureAppConfiguration() to access the config then use it to continue building the IWebHostBuilder.
For example:
public static async Task Main(string[] args)
{
var hostBuilder = Host.CreateDefaultBuilder(args);
var builder = hostBuilder
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
// make sure to put this at the end of this method
webBuilder.ConfigureAppConfiguration(configBuilder =>
{
// compile a temporary configuration instance
var configuration = configBuilder.Build();
// Check config values as you would normally
var myConfig = new MyConfig();
configuration.Bind("MyConfigSection", myConfig);
if (myConfig.UseSentryLogging)
{
// configure the web host based on config
webBuilder.UseSentry();
}
});
});
var host = builder.Build();
await host.RunWithTasksAsync();
}
If you are configuring other services which can affect where configuration is read from, then you may find you need to store a reference to the IWebHostBuilder and call ConfigureAppConfiguration on the IHostBuilder instead. Make sure you call ConfigureAppConfiguration last so that the other configuration setup can take place first and you access it correctly:
public static async Task Main(string[] args)
{
// we store the reference to the webHostBuilder so we can access it outside of ConfigureWebHost
IWebHostBuilder _webBuilder = null;
var hostBuilder = Host.CreateDefaultBuilder(args);
var builder = hostBuilder
.ConfigureWebHostDefaults(webBuilder =>
{
// store the builder for later
_webBuilder = webBuilder;
webBuilder.UseStartup<Startup>();
})
.ReadConfigFromSomewhereElse()
.ConfigureAppConfiguration(configBuilder =>
{
// compile a temporary configuration instance
var configuration = configBuilder.Build();
// Check config values as you would normally
var myConfig = new MyConfig();
configuration.Bind("MyConfigSection", myConfig);
if (myConfig.UseSentryLogging)
{
// configure the web host based on config
_webBuilder.UseSentry();
}
});
var host = builder.Build();
await host.RunWithTasksAsync();
}
Note this is a little bit of a hack and so may not work in all cases.
The right question is, do you really need a configuration for your configuration? You can use another file, and bind it directly with entity
configuration.GetSection("entity").Bind(entity);
If I didn't understand what you mean, all I know is that the setup you will need it for dependency injection, or you can't use it, so instead of using this, you can use Extensions for your service collection to do what you want with your configuration, and you get the setup in your container.
By default ConfigureAppConfiguration() will load appsettings.json according to the documentation Default builder settings.
Alternatively, if you can't access your configuration or you are getting System.NullReferenceException: 'Object reference not set to an instance ofan object.' error, it means the IHostBuilder can't see the json file in it base path.
Check or change the appsettings.json file properties as seen in the image below:
Set "Build Action" to "Content"
Set "Copy to Output Directory" to "Copy if newer"

Getting reference of Startup.cs object

Here's the skeleton of a standard ASP.NET Core application:
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.Build();
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
In this piece the ASP.NET Core apparatus instantiates an instance of Startup.cs class
.UseStartup<Startup>()
My query is how can I get hold (reference) of this already instantiated instance of Startup object that I can plug into my Library/Framework.
Context is to setup some Uber level framework and get a reference of this junction (Startup.cs) where all the requests are getting initiated.
If your Startup implements IStartup interface, getting reference to it is easy:
var host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
var startup = host.Services.GetService(typeof(IStartup)); // or from any other part of code using IServiceProvider.
However, asp.net core does not require your startup class to implement this interface. If it does not - it will use adapter pattern and adapt your Startup class to IStartup interface. You will still have an instance of IStartup, but it will not be your Startup class. Instead it will be an instance of ConventionBasedStartup. Asp.net core will explore methods of your startup class, find Configure and ConfigureServices methods and will pass them to ConventionBasedStartup which will adapt them to IStartup interface. In this case, it's not possible to retrieve instance of your startup class without heavy reflection, because it's not actually stored in any field (even in private) of ConventionBasedStartup and is only reachable through delegate references.
Long story short - if you want to get instance of your Startup class - make it implement IStartup interface.
Update about how to implement IStartup interface:
public class Startup : IStartup
{
public Startup(IHostingEnvironment env)
{
// constructor as usual
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void Configure(IApplicationBuilder app) {
app.UseMvc();
// resolve services from container
var env = (IHostingEnvironment) app.ApplicationServices.GetService(typeof(IHostingEnvironment));
var logger = (ILoggerFactory)app.ApplicationServices.GetService(typeof(ILoggerFactory));
logger.AddConsole(Configuration.GetSection("Logging"));
logger.AddDebug();
// etc
}
public IServiceProvider ConfigureServices(IServiceCollection services) {
services.AddMvc();
// etc
// return provider
return services.BuildServiceProvider();
}
}

.NET core Pass Commandline Args to Startup.cs from Program.cs

I'm trying to configure kestrel so that when it's in it's raw mode it runs on a specific port. However to do so it appears that the launchsettings.json needs to pass command line args to do so since there is no straight up option and it always runs on port 5000 which will obviously conflict if you have an api you need to run and a website.
So I added the CommandLine package to my site and you can indeed use builder.AddCommandLine() in the startup.cs file.
The problem is how to get the args from the program.cs to the Startup.cs or look them up other than a static variable.
Kind of makes the extension method pointless if you can't get at the args.
Any better ways of doing this?
A simple solution is to access the command line arguments through the Environment.GetCommandLineArgs method.
You only need to make sure that you remove the first argument, which is the executable name:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
var builder = new ConfigurationBuilder();
builder.AddCommandLine(args);
Configuration = builder.Build();
}
}
UPDATE
I actually found what seems more elegant solution:
Parse command line arguments into IConfigurationRoot in Program (using CommandLineApplication, good article & examples here)
Just pass this IConfigurationRoot to Startup via DI container.
Like so:
public static IWebHost BuildWebHost(string[] args)
{
var configuration = LoadConfiguration(args);
// Use Startup as always, register IConfigurationRoot to services
return new WebHostBuilder()
.UseKestrel()
.UseConfiguration(configuration)
.ConfigureServices(s => s.AddSingleton<IConfigurationRoot>(configuration))
.UseStartup<Startup>()
.Build();
}
public class Startup
{
public Startup(IConfigurationRoot configuration)
{
// You get configuration in Startup constructor or wherever you need
}
}
Example implementation of LoadConfiguration, which parses args and builds IConfigurationRoot (in this example configuration file name can be overridden in command line arguments):
private static IConfigurationRoot LoadConfiguration(string[] args)
{
var configurationFileName = "configuration.json";
var cla = new CommandLineApplication(throwOnUnexpectedArg: true);
var configFileOption = cla.Option("--config <configuration_filename>", "File name of configuration", CommandOptionType.SingleValue);
cla.OnExecute(() =>
{
if (configFileOption.HasValue())
configurationFileName = configFileOption.Value();
return 0;
});
cla.Execute(args);
return new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile(configurationFileName, optional: false, reloadOnChange: true)
.AddCommandLine(args)
.Build();
}
OLD ANSWER
You can instantiate Startup class by yourself and pass it as instances to WebHostBuilder. It is somewhat not so elegant, but doable. From here.
public static IWebHost BuildWebHost(string[] args)
{
// Load configuration and append command line args
var config = new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile("configuration.json")
.AddCommandLine(args)
.Build();
// pass config to Startup instance
var startup = new Startup(config);
// Instead of using UseStartup<Startup>()
// Register startup to services
return new WebHostBuilder()
.UseKestrel()
.UseSetting("applicationName", "Your.Assembly.Name")
.UseConfiguration(config)
.ConfigureServices(services => services.AddSingleton<IStartup>(startup))
.Build();
}
Couple of caveats are:
by doing so, Startup should implement IStartup which is limited for Configure method in parameters to only Configure(IApplicationBuilder app) instead of full Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime lifetime)
For some reason, you need to specify applicationName parameter manually as in my example. I'm testing this on 2.0.0-preview1-final
Kestrel can be configured to listen on a different port in several ways. None of these methods need to happen in the Startup class, but rather in the Main method of the Program class. Using the AddCommandLine extension method is one of them. To use that, modify your Program.cs file's Main method to look something like this:
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.Build();
var host = new WebHostBuilder()
.UseKestrel()
.UseConfiguration(config)
.UseStartup<Startup>()
.Build();
host.Run();
}
Then, run the application with dotnet run --server.urls http://*:<yourport>, replacing <yourport> with the actual port number that you want it to run on. The * makes it listen on all available IP addresses, if you want to listen on a specific address then you need to specify it there instead of the *.
Another option for changing the port is to use the .UseUrls method to hard-code the port and address. For example:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://*:8080")
.UseStartup<Startup>()
.Build();
host.Run();
}
This example will make your application listen on port 8080 on all available IP addresses.

Categories