Alter DbContext connection string for integration tests - c#

When running integration tests, I would like to use a partially randomly generated connection string for the DbContext where the database part of the connection string has a format like AppName_{RandomGuid}. This would allow multiple integration test instances to run at the same time against a real database without anyone stepping on anyone else, as the integration test databases would be created, and destroyed as needed on the central dev database server. The entire connection string would look like Server=tcp:0.0.0.0,49172;Database=AppName_{RandomGuid};User Id=;Password=;TrustServerCertificate=True.
What I have tried is something like this with the WebApplicationFactory:
private static readonly WebApplicationFactory<Program> WebApplication = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
var testConfigurationSettings = new Dictionary<string, string>
{
{ $"Database:ConnectionString", $"Server=tcp:0.0.0.0,49172;Database=AppName_{Guid.NewGuid()};User Id=;Password=;TrustServerCertificate=True" }
};
var testConfiguration = new ConfigurationBuilder()
.AddInMemoryCollection(testConfigurationSettings)
.Build();
builder.UseConfiguration(testConfiguration);
});
This would work expect that in my Program.cs I have this line:
var builder = WebApplication.CreateBuilder(args);
// THIS LINE
builder.Configuration.AddJsonFile(path: $"appsettings.Development.{Environment.MachineName}.json", optional: true);
builder.Services.AddApplicationServices(builder.Configuration);
var app = builder.Build();
await app.RunAsync();
This allows developers (based on their machine name) to override their own settings as they want when running the application locally; importantly, each dev will have their own specific config file that, at a minimum, has a connection string which points to their own dev specific database. My problem with this solution is the line which adds the developer specific config is overriding the config registered in the WithWebHostBuilder for the integration test, because, the Program.cs executes after the WithWebHostBuilder code.
The first part is my actual problem, and the second is my current attempted solution which is so close, and clean to doing exactly what I want.

This is the solution I am using for now that works, but might be considered a bit hacky. If no other solutions come soon, then I'll mark this as the accepted answer.
Basically, I added a field to my appsettings called IsTesting, and by default it is false, but is set to true when running the integration tests:
private static readonly WebApplicationFactory<Program> WebApplication = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
var testConfigurationSettings = new Dictionary<string, string>
{
{ "IsTesting", "true" },
{ $"Database:ConnectionString", $"Server=tcp:0.0.0.0,49172;Database=AppName_{Guid.NewGuid()};User Id=;Password=;TrustServerCertificate=True" }
};
var testConfiguration = new ConfigurationBuilder()
.AddInMemoryCollection(testConfigurationSettings)
.Build();
builder.UseConfiguration(testConfiguration);
});
This way I can check for the setting in my Program.cs and skip applying the developer configuration files if needed that were previously overriding the integration test settings:
var config = configurationManager.Get<ApplicationConfiguration>();
if (config.IsTesting)
{
return;
}
configurationManager.AddJsonFile(path: $"appsettings.Development.{Environment.MachineName}.json", optional: true);

Related

Access ASP.NET Core settings during service configuration when using minimal api

I am trying to access my settings in order to configure my services. I am using AWS systems manager to store my settings
// Settings class
public class Settings
{
public string Url { get; set; }
}
// my Program.cs code
var builder = WebApplication.CreateBuilder();
// Clear default sources
builder.Configuration.Sources.Clear();
// Get configuration
builder.Configuration.AddSystemsManager(source =>
{
source.Path = $"/{env}";
source.ParameterProcessor = new DefaultParameterProcessor();
});
// Load up the settings
builder.Services.Configure<Settings>(builder.Configuration.GetSection(appName));
// Access the settings to use for configuring the services
var settings = builder.Configuration.Get<Settings>();
When I check the settings object, the values are null. However when I inject the settings into an endpoint using IOptions<Setting>, the values are populated.
I tried the .Get<Settings> after I called Build();, but that also returned null values. Is there something I am missing?
You said you want to access the settings to configure some services. You can accomplish that with something like:
builder.Services.AddSingleton<IMyService>(x => {
// x is IServiceProvider
var settings = x.GetRequiredService<IOptions<Settings>>();
var url = settings.Value.Url;
if(url is null)
throw new Exception("missing url");
var myservice = new MyService(url);
// this is the instance that will be used, use singleton/scoped/transient as needed
return myservice;
});
where MyService is the service you want to configure, if not needed you can avoid the IMyService interface and of course you need to use the desired lifetime (singleton/scoped/transient)
Doing so though will somehow force you to explicitly declare the dependencies of "MyService".
Another way of fetching that value is:
// get the configuration section
var section = builder.Configuration.GetSection("appname:Settings");
// create the POCO
var settings = new Settings();
// bind the values to the POCO
section.Bind(settings);
// you can access the value
var yourUrl = settings.Url;
// you can still inject it as IOption<>
builder.Services.Configure<Settings>(section);
I am assuming you have an "appname" section with a "Settings" section in your configuration, if not then use .GetSection("appname"); instead of .GetSection("appname:Settings");
According to the [docs], you should be able to retrieve your options using extension method:
builder.Configuration.GetAWSOptions()
As an alternative, if you have those Settings options in appsettings.json you should be able to retrieve those using: builder.Configuration["Settings:Url"]
Also, as you have used builder.Configuration.GetSection(appName) doesn't it work for you when you try to retrieve options using:
builder.Configuration.GetSection("Settings")?

Is it possible to programmatically configure NLog when used with Microsoft.Extensions.DependencyInjection?

I'm using Microsoft.Extensions.DependencyInjection in my Project and want to use Microsoft.Extension.Logging, with NLog as the actual Logging Framework.
Since I want to be able to decide the Logging Configuration on Startup programmatically (to get around pathing issues) i'm doing something like this:
var config = new LoggingConfiguration();
var dirPath = Path.Combine(publicStorage, "Test");
var logFileTarget = new NLog.Targets.FileTarget()
{
CreateDirs = true,
FileName = Path.Combine(dirPath, "test.log"),
FileNameKind = NLog.Targets.FilePathKind.Absolute,
Layout = "${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}",
Name = "FileLog",
};
//Add Logging Targets
config.AddTarget(logFileTarget);
//Add Rules
config.AddRuleForAllLevels(logFileTarget, "*", false);
LogManager.Configuration = config;
But this does not configure those Loggers returned by ServiceProvider.GetService<ILogger<T>>.
If I however use LogManager.GetLogger("TestLogger") The Logging is configured as would be expected.
LogManager.ReconfigExistingLoggers(); doesn't solve the Problem as well and my search came up empty
Edit
The DI Setup looks like this:
return new ServiceCollection()
// Configure Logging Provider
.AddLogging(builder =>
{
builder.AddDebug();
//Add NLog
builder.AddNLog(new NLogProviderOptions()
{
CaptureMessageTemplates = true,
CaptureMessageProperties = true,
});
})
... // Registering additional dependencies
.BuildServiceProvider();
Using LogManager.GetCurrentClassLogger gives the same "incorrect behaviour" as seen with the Loggers returned by serviceProvider. Since
the rule is setup with "*" I would have expected that all loggers including those using class names are caught by this rule
This last sentence is incorrect it is actually working, but the non-appearance of ILogger by di messages still exists
Remember the missing:
builder.SetMinimumLevel(LogLevel.Trace);
See also wiki: https://github.com/NLog/NLog.Extensions.Logging/wiki/Getting-started-with-.NET-Core-2---Console-application#32-setup-the-dependency-injector-di-container

adding profiles to automapper after initialisation

having some trouble with adding profiles after some external assembly already added mapperconfiguration on the DI-setup.
first i just added some code to add the profiles
var mapperConfiguration = new MapperConfiguration(cfg =>
{
cfg.AddProfile<DataMappingProfile>();
});
mapperConfiguration.AssertConfigurationIsValid();
services.AddSingleton<IMapper>(new Mapper(mapperConfiguration));
but then i overwrote some other mappingprofiles.
so i was thinking, i should try to add mine to the existing mappingconfiguration.
so i was going this way
var sp = services.BuildServiceProvider();
var autoMapper = sp.GetService<IMapper>();
var mapperConfiguration = autoMapper?.ConfigurationProvider as MapperConfiguration;
var configuration = new MapperConfigurationExpression();
configuration.AddProfile<LpisMappingProfile>();
if (mapperConfiguration == null)
{
mapperConfiguration = new MapperConfiguration(configuration);
}
else
{
//add the previous as well
//?? add this `configuration` ?
}
mapperConfiguration.AssertConfigurationIsValid();
services.AddSingleton<IMapper>(new Mapper(mapperConfiguration));
but i am a little stuck on the else flow.
Any advice?
thnx!
That's not possible. If an assembly uses a private MapperConfiguration, then that's its own business. If it wants to collaborate with the app, it should not define a MapperConfiguration, it should define profiles to be scanned by the app and added to the singleton MapperConfiguration owned by the app. The AM configuration is read-only, after the init phase it's not possible to change it.

IWebHostBuilder.GetSetting() doesnt have appsettings.json data

I am working on refactoring an ASP.Net Core v1 application to v2. The gist of the current effort is moving the database seeding logic to Program.Main() as indicated in the MS docs...
In 2.0 projects, move the SeedData.Initialize call to the Main
method of Program.cs:
The problem I am encountering is that I need to get a config flag out the appsettings.json file. This appears to be loaded by default when WebHost.CreateDefaultBuilder is called as per the source, but my code isn't able to retrieve a simple flag from my appsettings.json.
public static IWebHost BuildWebHost(string[] args)
{
var builder = WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
string env = builder.GetSetting("environment");
var envMsg = "ASPNETCORE_ENVIRONMENT/Environment variable ";
if (string.IsNullOrWhiteSpace(env))
throw new ArgumentNullException("environment", envMsg + "missing!");
else
Console.WriteLine(envMsg + "found!");
string doSeed = builder.GetSetting("SeedDb");
var seedMsg = "SeedDb in appsettings.json ";
if (string.IsNullOrWhiteSpace(doSeed))
throw new ArgumentNullException("SeedDb", seedMsg + "missing!");
else
Console.WriteLine(seedMsg + "found!");
return builder.Build();
}
The environment variable is set (as expected), but the values from the json file do not appear to be. No exception on env check, but there is on the seed flag.
Repo here to verify if needed. What am I missing? I've seen mention of prefixing the setting to search for with "AppSettings", etc... but I don't see that indicated in the source (and can't verify what it should be, if anything.)
Combining the results from this SO, I have it figured out...
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var config = services.GetService<IConfiguration>(); // the key/fix!
var s = config.GetValue<string>("SeedDb");
var doSeed = bool.Parse(s); // works!
}
host.Run();
}
Or just var doSeed = config.GetValue<bool>("SeedDb");!

Using Hangfire, connection string given in Startup.cs throws Cannot attach file as database error

I'm utilizing Hangfire in my ASP .Net MVC Web App, it had installed successfully. I'd like to use the same LocalDb to store queued jobs for Hangfire to dequeue and process as I've used to stored data. However I'm running into the below error when I provided its connectionString or name defined in Web.config in Startp.cs. I've had no trouble adding, deleting updating data in the same localDb before hangfire.
Cannot attach the file 'c:\users\jerry_dev\documents\visual studio 2013\Projects\Hangfire.Highlighter\Hangfire.Highlighter\App_Data\aspnet-Hangfire.Highlighter-20150113085546.mdf' as database 'aspnet-Hangfire.Highlighter-20150113085546'.
Startup.cs:
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
app.UseHangfire(config =>
{
string hangfireConnectionString = #"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-Hangfire.Highlighter-20150113085546.mdf;Initial Catalog=aspnet-Hangfire.Highlighter-20150113085546;Integrated Security=True";
config.UseSqlServerStorage(hangfireConnectionString);
config.UseServer();
});
}
My project Solution is named "Hangfire.Highlighter"
Web.config:
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-Hangfire.Highlighter-20150113085546.mdf;Initial Catalog=aspnet-Hangfire.Highlighter-20150113085546;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
I know this is old - but its been 9 months and I pulled my hair out over this as well - and decided to do a write-up on it here.
My solution was to just create a quick and dirty DbContext, point it to the proper connection string, and call Database.CreateIfNotExists in the constructor:
public class HangfireContext : DbContext
{
public HangfireContext() : base("name=HangfireContext")
{
Database.SetInitializer<HangfireContext>(null);
Database.CreateIfNotExists();
}
}
In the HangfireBootstrapper.Start() method I do something like this:
public void Start()
{
lock (_lockObject)
{
if (_started) return;
_started = true;
HostingEnvironment.RegisterObject(this);
//This will create the DB if it doesn't exist
var db = new HangfireContext();
GlobalConfiguration.Configuration.UseSqlServerStorage("HangfireContext");
// See the next section on why we set the ServerName
var options = new BackgroundJobServerOptions()
{
ServerName = ConfigurationManager.AppSettings["HangfireServerName"]
};
_backgroundJobServer = new BackgroundJobServer(options);
var jobStarter = DependencyResolver.Current.GetService<JobBootstrapper>();
//See the Recurring Jobs + SimpleInjector section
jobStarter.Bootstrap();
}
}
Not sure why Hangfire has such a hard time with LocalDb - maybe it can only handle full-blown SQL instances? Either way this works for me, new team members, and new dev/staging/prod instances that get stood up.
I too know this is old, but ran into this recently. Here is my fix:
In Visual Studio, go to 'View -> SQL Server Object Explorer'
Connect to the Data Source if it isn't already connected. In the example above it was '(LocalDb)\v11.0'
Right click on 'Databases' -> 'Add New Database'
Fill Database name = Ex: 'aspnet-Hangfire.Highlighter-20150113085546' or whatever you've named the database in the connection string.
Fill Database location = This should be the Data Directory in your application, 'App_Data' for the MVC project.
This fixed the issue in my case.
Jack's answer didn't work for me, because I ran into this problem: No connection string named could be found in the application config file
I got it to work with the following modifications:
Remove "name=" from the string in the base initializer. Thanks to: https://stackoverflow.com/a/37697318/2279059
This moves the error to the call of UseSqlServerStorage. So instead of passing "HangfireContext" to it, I just copy the connection string from the dummy database context.
Complete setup code:
public class HangfireContext : DbContext
{
public HangfireContext() : base("HangfireContext") // Remove "name="
{
Database.SetInitializer<HangfireContext>(null);
Database.CreateIfNotExists();
}
}
public partial class Startup
{
public static void ConfigureHangfire(IAppBuilder app)
{
var db = new HangfireContext();
GlobalConfiguration.Configuration.UseSqlServerStorage(db.Database.Connection.ConnectionString); // Copy connection string
app.UseHangfireDashboard();
app.UseHangfireServer();
}
}
Is the DB already created? Can you try using a different conneciton string format?something like this,
"Server=.;Database=HangFire.Highlighter;Trusted_Connection=True;"
Answer as per AspNetCore 3.1 and Hangfire 1.7.17
Hangfire should create all the tables provided there is an existing database with the specified database name.
If you want to use LocalDb, you can use the following registrations (see below).
services
.AddHangfire(
(serviceProvider, config) =>
{
//read settings or hardcode connection string, but this is cleaner
var configuration = serviceProvider.GetService<IConfiguration>();
var connectionString = configuration.GetValue<string>("Hangfire:ConnectionString");
var sqlServerStorageOptions =
new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
};
config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings();
.UseSqlServerStorage(connectionString, sqlServerStorageOptions);
});
The connection string is read, in my example, from the appsettings, so it'd look like this
"Hangfire": {
"ConnectionString": "Data Source=(localdb)\\MsSqlLocalDb; Database=Hangfire;"
}
Again, notice how the connection string has the name of the database (e.g: Hangfire) that MUST exist in localdb. If you remove the Database=xxx parameter altogether, it'll pick the master database by default and create all the tables there.

Categories