Overriding database provider in integration test with WebApplicationFactory - c#

I am following the official MS documentation for integration testing .Net Core (https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1).
I was able to get the first part of the integration test done where I was not overriding the startup class of the application I am testing (i.e. I was using a web application factorythat did not override any services).
I want to override the database setup to use an in-memory database for the integration test. The problem I am running into is that the configuration continues to try and use the sql server for services.AddHangfire().
How do I override only above specific item in my integration test? I only want to override the AddHangfire setup and not services.AddScoped<ISendEmail, SendEmail>(). Any help would be appreciated.
Test Class with the custom web application factory
public class HomeControllerShouldCustomFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public HomeControllerShouldCustomFactory(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task IndexRendersCorrectTitle()
{
var response = await _client.GetAsync("/Home/Index");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Send Email", responseString);
}
}
Custom Web Application Factory
public class CustomWebApplicationFactory<TStartup>: WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My startup.cs in my application that I am testing
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ISendEmail, SendEmail>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
RecurringJob.AddOrUpdate<ISendEmail>((email) => email.SendReminder(), Cron.Daily);
app.UseMvc();
Update
I don't see this issue in my other example project where I am using only entity framework. I have a simple application with an application db context which uses SQL server. In my test class, I override it with an in-memory database and everything works. I am at a loss at to why it will work in my example application but not work in my main application. Is this something to do with how HangFire works?
In my test application (example code below), I can delete my sql database, run my test, and the test passes because the application DB context does not go looking for the sql server instance but uses the in-memory database. In my application, the HangFire service keeps trying to use the sql server database (if I delete the database and try to use an in-memory database for the test - it fails because it can't find the instance its trying to connect to). How come there is such a drastic difference in how the two projects work when a similar path is used for both?
I ran through the debugger for my integration test which calls the index method on the home controller above (using the CustomWebApplicationFactory). As I am initializing a test server, it goes through my startup class which calls below in ConfigureServices:
services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
After that, the Configure method tries to call below statement:
app.UseHangfireServer();
At this point the test fails as It cannot find the DB. The DB is hosted on Azure so I am trying to replace it with an in-memory server for some of the integration test. Is the approach I am taking incorrect?
My example application where its working
Application DB Context in my example application
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Message> Messages { get; set; }
public async Task<List<Message>> GetMessagesAsync()
{
return await Messages
.OrderBy(message => message.Text)
.AsNoTracking()
.ToListAsync();
}
public void Initialize()
{
Messages.AddRange(GetSeedingMessages());
SaveChanges();
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "You're standing on my scarf." },
new Message(){ Text = "Would you like a jelly baby?" },
new Message(){ Text = "To the rational mind, nothing is inexplicable; only unexplained." }
};
}
}
Startup.cs in my example application
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
CustomWebApplicationFactory - in my unit test project
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(serviceProvider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
});
}
}
My unit test in my unit test project
public class UnitTest1 : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Startup> _factory;
public UnitTest1(CustomWebApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async System.Threading.Tasks.Task Test1Async()
{
var response = await _client.GetAsync("/");
//response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Home", responseString);
}
Update 2
I think I found an alternate to trying to override all my configuration in my integration test class. Since it's a lot more complicated to override HangFire as opposed to an ApplicationDBContext, I came up with below approach:
Startup.cs
if (Environment.IsDevelopment())
{
var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
services.AddHangfire(x => x.UseStorage(inMemory));
}
else
{
services.AddHangfire(x => x.UseSqlServerStorage(Configuration["DBConnection"]));
}
Then in my CustomWebApplicationBuilder, I override the environment type for testing:
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<SendGridExample.Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development"); //change to Production for alternate test
builder.ConfigureServices(services =>
{
// Create a new service provider.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
});
}
}
With that approach, I don't need to worry about having to do extra logic to satisfy hangfire's check for an active DB. It works but I am not 100% convinced its the best approach as I'm introducing branching in my production startup class.

There are two different scenarios you need to check.
Create a job by class BackgroundJob
Create a job by interface IBackgroundJobClient
For the first option, you could not replace the SqlServerStorage with MemoryStorage.
For UseSqlServerStorage, it will reset JobStorage by SqlServerStorage.
public static IGlobalConfiguration<SqlServerStorage> UseSqlServerStorage(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] string nameOrConnectionString)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));
var storage = new SqlServerStorage(nameOrConnectionString);
return configuration.UseStorage(storage);
}
UseStorage
public static class GlobalConfigurationExtensions
{
public static IGlobalConfiguration<TStorage> UseStorage<TStorage>(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] TStorage storage)
where TStorage : JobStorage
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
if (storage == null) throw new ArgumentNullException(nameof(storage));
return configuration.Use(storage, x => JobStorage.Current = x);
}
Which means, no matter what you set in CustomWebApplicationFactory, UseSqlServerStorage will reset BackgroundJob with SqlServerStorage.
For second option, it could replace IBackgroundJobClient with MemoryStorage by
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<JobStorage>(x =>
{
return GlobalConfiguration.Configuration.UseMemoryStorage();
});
});
}
}
In conclusion, I suggest you register IBackgroundJobClient and try the second option to achieve your requirement.
Update1
For DB is not available, it could not be resolved by configuring the Dependency Injection. This error is caused by calling services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));.
For resolving this error, you need to overriding this code in Startup.cs.
Try steps below:
Change Startup to below:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//Rest Code
ConfigureHangfire(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//Rest Code
app.UseHangfireServer();
RecurringJob.AddOrUpdate(() => Console.WriteLine("RecurringJob!"), Cron.Minutely);
}
protected virtual void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(config =>
config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"))
);
}
}
Create StartupTest in test project.
public class StartupTest : Startup
{
public StartupTest(IConfiguration configuration) :base(configuration)
{
}
protected override void ConfigureHangfire(IServiceCollection services)
{
services.AddHangfire(x => x.UseMemoryStorage());
}
}
CustomWebApplicationFactory
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint: class
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder(null)
.UseStartup<TEntryPoint>();
}
}
Test
public class HangfireStorageStartupTest : IClassFixture<CustomWebApplicationFactory<StartupTest>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<StartupTest> _factory;
public HangfireStorageStartupTest(CustomWebApplicationFactory<StartupTest> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
}

Related

Hangfire recurring job to call method is Startup using existing services

I'm trying to setup Hangfire to run a recurring job within Startup.cs with a standalone method. For this to work, I need to grab some ApplicationServices I have injected already. But the job execution fails with this error:
Recurring job 'Job: Dispatch Email from Queue' can't be scheduled due to an error and will be retried in 00:00:15.
System.InvalidOperationException: Recurring job can't be scheduled, see inner exception for details.
---> Hangfire.Common.JobLoadException: Could not load the job. See inner exception for the details.
---> Newtonsoft.Json.JsonSerializationException: Could not create an instance of type DA.BrrMs.Application.Interfaces.ServiceInterfaces.IBudgetReleaseRequestService. Type is an interface or abstract class and cannot be instantiated.
Here is what I have:
public class Startup
{
private IConfiguration Configuration { get; }
private RecurringJobManager _jobManager;
public Startup(IConfiguration configuration) => Configuration = configuration;
public void ConfigureServices(IServiceCollection services)
{
...
if (Configuration["SystemSettings:UseHangfire"] == "1")
{
services.AddHangfire(c => c.UseMemoryStorage());
JobStorage.Current = new MemoryStorage();
services.AddHangfireServer();
GlobalConfiguration.Configuration.UseMemoryStorage();
}
RegisterServices(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
var requestService = app.ApplicationServices.GetService<IBudgetReleaseRequestService>();
var mailService = app.ApplicationServices.GetService<IMailer>();
_jobManager = new RecurringJobManager();
_jobManager.AddOrUpdate("Job: Dispatch Email from Queue", () => StartupMailJob(requestService, mailService), Cron.Minutely);
}
private static void RegisterServices(IServiceCollection services) => DependencyContainer.RegisterServices(services);
public static async Task StartupMailJob(IBudgetReleaseRequestService requestService, IMailer mailService)
{
try
{
var emailsToSend = await requestService.DispatchEmails();
// Send emails
foreach (var emailToSend in emailsToSend)
{
}
// Update status of sent
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
// In a project, far far away...
public static void RegisterServices(IServiceCollection services)
{
...
services.AddScoped<IBudgetReleaseRequestService, BudgetReleaseRequestService>();
...
}
How do I satisfy Hangfire's requirment of a class instance instead of an interface?
Here is the implementation that I found best worked for me:
1 Register the service I wanted to schedule in Startup.cs
services.AddScoped<IMyJob, MYJob>();
2 Created an activator class that is used to define the scope for the execution of the job.
public class HangFireActivatorMyJob : IHangFireActivatorMyJob
{
private readonly IServiceProvider _serviceProvider;
public HangFireActivatorMyJob (IServiceProvider serviceProvider)
{
this._serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public async Task Run(IJobCancellationToken token)
{
token.ThrowIfCancellationRequested();
await RunAtTimeOf(DateTime.Now);
}
public async Task RunAtTimeOf(DateTime now)
{
using IServiceScope scope = this._serviceProvider.CreateScope();
var myJobService= scope.ServiceProvider.GetRequiredService<IMyJob>();
await myJobService.RunSomeTaskOnJob();
}
}
3 Register the activator
services.AddTransient<IHangFireActivatorMyJob, HangFireActivatorMyJob >();
4 Add the IRecurringJobManager interface to the configure method within Startup.cs
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IRecurringJobManager recurringJobManager)
5 Add the job to the provided IRecurringJobManager
recurringJobManager.AddOrUpdate<HangFireActivatorMyJob >(nameof(HangFireActivatorMyJob ),
job => serviceProvider.GetRequiredService<IHangFireActivatorMyJob>()
.Run(JobCancellationToken.Null)
, Cron.Hourly(3), TimeZoneInfo.Utc);

Readiness Healthcheck with warmup C#

I am using Readiness healthchecks for my project and want to add a warmup period to it.
Dependency Injection is being used to get the warmup task from the Kernel but I am not able to get it because the Readiness Healthcheck is being initialized before the IKernel it seems.
I am getting the follow error:
Unable to resolve service for type 'IKernel' while attempting to activate 'Project.Ranking.API.HealthCheck.RankingReadinessHealthCheck'.
How does one use a class to warm up the pod before it is being used.
I have not been able to find a working example where someone warms up before the endpoints are available.
UPDATE:
Core.Library Startup.CS
public void CoreConfigureServices(IServiceCollection services)
{
... other code
services.AddHealthChecks()
.AddIdentityServer("https://identity.example.com")
.AddCheck<IReadinessHealthCheck>("Readiness", failureStatus: null)
.AddCheck<ILivenessHealthCheck>("Liveness", failureStatus: null);
services.AddSingleton<ILivenessHealthCheck, LivenessHealthCheck>();
}
public void CoreConfigure(IApplicationBuilder app, IHostEnvironment env)
{
... other code
app.UseHealthChecks("/healthcheck/live", new HealthCheckOptions()
{
Predicate = check => check.Name == "Liveness"
});
app.UseHealthChecks("/healthcheck/ready", new HealthCheckOptions()
{
Predicate = check => check.Name == "Readiness",
});
}
API Startup.CS
public void ConfigureServices(IServiceCollection services)
{
CoreConfigureServices(services);
... other code
services.AddSingleton<Core.Library.IReadinessHealthCheck, ReadinessHealthCheck>();
}
public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
CoreConfigure(app, env);
... other code
//Here used to be the warm up, but this is used in the liveness probe and i want to warm up in the readiness probe
//Kernel.Get<IWarmupTask>().Initialize();
Kernel.Bind<IReadinessHealthCheck>().To<ReadinessHealthCheck>();
}
Core.Library BaseReadinessHealthCheck.cs
public abstract class BaseReadinessHealthCheck : IReadinessHealthCheck
{
public BaseReadinessHealthCheck()
{
}
private bool StartupTaskCompleted { get; set; } = false;
public abstract void WarmUp();
public void CompleteTask()
{
StartupTaskCompleted = true;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
//start tasks
if (!StartupTaskCompleted)
{
Task.Run(() => WarmUp());
}
if (StartupTaskCompleted)
{
return Task.FromResult(HealthCheckResult.Healthy("The startup task is finished."));
}
return Task.FromResult(HealthCheckResult.Unhealthy("The startup task is still running."));
}
}
API ReadinessHealthCheck.CS
public class ReadinessHealthCheck : ReadinessHealthCheck
{
public ReadinessHealthCheck(IKernel kernel) : base(kernel)
{
}
public override void WarmUp()
{
// I want to do a warmup here, where it calls IWarmupTask
CompleteTask();
}
}

how to configure dependencies in asp.net core

I have an ASP.Net Core 2.1 application. I need to register & inject few dependencies of AWS.
Currently, the implementation looks like this:
public abstract class BaseService
{
protected readonly IConfiguration _configuration;
protected readonly RegionEndpoint _region;
protected readonly IAmazonDynamoDB _dynamoClient;
protected IPocoDynamo _pocoDynamo;
public BaseService(IConfiguration configuration)
{
_configuration = configuration;
var awsSettings = configuration.GetSection("AWS");
_dynamoClient = SetDynamoClient(awsSettings);
_pocoDynamo = SetPocoDynamoClient();
}
protected IAmazonDynamoDB SetDynamoClient(IConfigurationSection configuration)
{
AWSCredentials credentials = new BasicAWSCredentials(configuration["AccessKey"], configuration["AccessSecret"]);
return new AmazonDynamoDBClient(credentials, _region);
}
protected IPocoDynamo SetPocoDynamoClient()
{
return new PocoDynamo(_dynamoClient);
}
}
While unit testing, AWS services can't be mocked due to this.
I want to register all these dependencies in Startup.cs in ConfigureServices()
This is what I was trying:
public void ConfigureServices(IServiceCollection services)
{
AWSCredentials credentials = new BasicAWSCredentials(configuration["AccessKey"], configuration["AccessSecret"]);
services.AddTransient(IAmazonDynamoDB, (a) =>
{
return new AmazonDynamoDBClient(credentials, RegionEndpoint.GetBySystemName(""))
});
// here I need to pass the IAmazonDynamoDB to below IOC
// services.AddSingleton<IPocoDynamo,new PocoDynamo()> ();
return services;
}
But this is throwing an error
Error CS0119 'IAmazonDynamoDB' is a type, which is not valid in the given context
How to configure dependencies as required here?
Thanks!
Use factory delegate to call the registered service
public void ConfigureServices(IServiceCollection services) {
AWSCredentials credentials = new BasicAWSCredentials(configuration["AccessKey"], configuration["AccessSecret"]);
services.AddTransient<IAmazonDynamoDB>(sp =>
new AmazonDynamoDBClient(credentials, RegionEndpoint.GetBySystemName(""))
);
//here pass the IAmazonDynamoDB to below IOC
services.AddSingleton<IPocoDynamo>(serviceProvider => {
var pocoDynamo = new PocoDynamo(serviceProvider.GetRequieredService<IAmazonDynamoDB>());
pocoDynamo.SomeMethod();
return pocoDynamo;
});
}
The target class should no longer need to be dependent on IConfiguration as the dependencies can be explicitly injected via constructor injection.
public abstract class BaseService {
protected readonly IAmazonDynamoDB dynamoClient;
protected readonly IPocoDynamo pocoDynamo;
public BaseService(IAmazonDynamoDB dynamoClient, IPocoDynamo pocoDynamo) {
this.dynamoClient = dynamoClient;
this.pocoDynamo = pocoDynamo;
}
}

Access DatabaseContext after Startup

I have created a basic DatabaseContext which handles the communication with a SQLite-Database:
public class DatabaseContext : DbContext
{
public DbSet<GearComponent> GearComponents{ get; set; }
public DatabaseContext() { }
public DatabaseContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename = database.db");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<GearComponent>().HasKey(m => m.Id);
base.OnModelCreating(builder);
}
}
I registered this DatabaseContext in Startup.cs like this:
services.AddDbContext<DatabaseContext>(options => options.UseSqlite("Filename=database.db"));
I created a database with this command:
dotnet ef migrations add testMigration
I also auto-created a controller to access the database via HTTP-GET/POST/PUT.
This controller gets an instance of the DatabaseContext:
public class GearComponentsController : ControllerBase
{
private readonly DatabaseContext _context;
public GearComponentsController(DatabaseContext context)
{
_context = context;
}
//...
}
This GearComponentsController mainly is for the frontend to receive the database entries. For the backend I don't want to go with HTTP-POST/GET etc. but instead I want to directly access the DatabaseContext - but how?
I tried this in Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
But my database.db never receives this entry.
Edit: For everyone who is interested in how I got around this, look here.
Since you asked how to work with DatabaseContext outside your controller to perform some business logic. You can use straightforward approach with repository pattern. Will simply demonstrate for inserting data. Assuming you have model GearComponent created and EntityFramework already seted up.
Create file which contains interface and class which implements this interface:
public interface IGearComponentRepository
{
void Create(GearComponent obj)
}
public class GearComponentRepository : IGearComponentRepository
{
private readonly DatabaseContext _context;
public GearComponentRepository (DatabaseContext context)
{
_context = context;
}
public void Create(GearComponent data)
{
_context.Add(data);
_context.SaveChanges();
}
}
You need to register this service via IoC container in you Startup.cs file
public void ConfigureServices(IServiceCollection services)
{
..
services.AddMvc();
services.AddTransient<IGearComponentRepository, GearComponentRepository>();
..
}
Then you can use Repositories from your Controller
public class GearComponentsController : ControllerBase
{
private readonly IGearComponentRepository _gearComponentRepository;
public GearComponentsController(IGearComponentRepository
_gearComponentRepository)
{
_gearComponentRepository = gearComponentRepository;
}
[HttpPost]
public IActionResult Create(GearComponent data)
{
_dataRepository.Create(data);
return Ok();
}
}
in program.cs:
public static void Main(string[] args)
{
var hostBuilder = CreateWebHostBuilder(args);
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (!string.IsNullOrEmpty(env) && env.Equals("Production"))
{
hostBuilder.ConfigureLogging((context, builder) =>
{
// Read GelfLoggerOptions from appsettings.json
builder.Services.Configure<GelfLoggerOptions>(context.Configuration.GetSection("Graylog"));
// Optionally configure GelfLoggerOptions further.
builder.Services.PostConfigure<GelfLoggerOptions>(options =>
options.AdditionalFields["machine_name"] = Environment.MachineName);
// Read Logging settings from appsettings.json and add providers.
builder.AddConfiguration(context.Configuration.GetSection("Logging"))
.AddConsole()
//.AddDebug()
.AddGelf();
});
}
var host = hostBuilder.Build();
using (var scope = host.Services.CreateScope())
{
try
{
// Retrieve your DbContext isntance here
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
if (env != null && !env.Equals("Production"))
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
}
else
{
dbContext.Database.SetCommandTimeout((int)TimeSpan.FromMinutes(10).TotalSeconds);
dbContext.Database.Migrate();
}
// place your DB seeding code here
//DbSeeder.Seed(dbContext);
}
catch (Exception ex)
{
Console.WriteLine(ex);
// Continue
}
}
host.Run();
}
Focusing on:
using (var scope = host.Services.CreateScope())
and
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
You will be able to access your dbContext just like that.
As in the docs described, the method CreateWebhostbuilder is used to differ between build host and run host.
To run the host means, the code after is as reachable as after a throw statement.
Try this:
public class Program
{
public static void Main(string[] args)
{
//use var host to build the host
var host = CreateWebHostBuilder(args).Build();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
// Run host after seeding
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Edit:
As far as I understand your issue now, you have a frontend, which should only consume the content of the database, if the refresh-button is clicked.
Along with that, your backend should consume other webservices and insert the consumed gearcomponents into the database.
In cause of the fact, that you don’t want another application do that job, which could be a windows-service (easy to handle intervals for updating the database),
the only way to trigger the Updates is to start them from the GearComponentsController or in an actionFilter. This way you can update your database and provide the data to the frontend. I hope, performance doesn’t matter.

Create and apply migrations for all databases in Entity Framework Core

I have ApplicationDbContext, which dynamically change connection string to database, which depends on user's library name. So, for each library, I have it's own database. When I'm creating migrations and apply them, they are related only to default database with default connection string, where no library name defined.
How can I make and apply migrations to all this databases, that dynamically created, that depends on library name (they exist after creation, they are fully defined and working databases)?
If I understood you properly, you should implement the following:
1) Implement Service that will migrate your schema and seed data. (despite what they did in the latest version, I still prefer my implementation)
Helper
public static async Task EnsureSeedData(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
// here could be several seed services if necessary
var seedService = scope.ServiceProvider.GetService<ISeedService>();
await seedService.MigrateAsync();
await seedService.SeedAsync();
}
}
ISeedService Interface
internal interface ISeedService
{
Task MigrateAsync();
Task SeedAsync();
}
SeedService Implementation
internal sealed class SeedService : ISeedService
{
private readonly InitializeContext _identityContext;
public SeedService(InitializeContext identityContext)
{
_identityContext = identityContext;
}
public async Task MigrateAsync()
{
await _identityContext.Database.MigrateAsync();
}
public async Task SeedAsync()
{
if (_identityContext.AllMigrationsApplied())
{
var strategy = _identityContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using (var transaction = await _identityContext.Database.BeginTransactionAsync())
{
// seed data if necessary
transaction.Commit();
}
});
}
}
}
Program.cs file
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
Task.Run(async () =>
{
await SeedData.EnsureSeedData(host.Services);
}).Wait();
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
2) Register context and related services in DI, which will allow dynamically retrieve connection string. There are several ways to do it, my implementation is not the best one, so it's up to you.
Startup.cs file
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISeedService, SeedService>();
services.AddScoped<IDbConfiguration, FakeDbConfiguration>();
services.AddDbContext<InitializeContext>((provider, builder) =>
{
var dbConfiguration = provider.GetService<IDbConfiguration>();
builder.UseSqlServer(dbConfiguration.Connection);
});
// omitted
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// omitted
}
}
DbConfiguration file.
internal sealed class FakeDbConfiguration : IDbConfiguration
{
private readonly IConfiguration _configuration;
public FakeDbConfiguration(IConfiguration configuration)
{
_configuration = configuration;
}
// REPLACE THIS PART WITH YOURS IMPLEMENTATION
public string Connection => _configuration["Database:Connection"];
I hope it help you.

Categories