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.
Related
I know how to seed data to a database with old .NET 5.0 in startup.cs file using my Seeder class with a Seed() method creating some initial data.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seeder seeder)
{
seeder.Seed();
..............
// other configurations
}
How do I do this in .NET 6.0? There is no place to add my Seeder class as an argument.
I solved a similar problem as follows:
Program.cs (.NET 6)
...
builder.Services.AddScoped<IDbInitializer, DbInitializer>(); //can be placed among other "AddScoped" - above: var app = builder.Build();
...
SeedDatabase(); //can be placed above app.UseStaticFiles();
...
void SeedDatabase() //can be placed at the very bottom under app.Run()
{
using (var scope = app.Services.CreateScope())
{
var dbInitializer = scope.ServiceProvider.GetRequiredService<IDbInitializer>();
dbInitializer.Initialize();
}
}
I have never use your solution before. This is what I'm doing,
public class DataContext: DbContext
{
public DataContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
new DbInitializer(modelBuilder).Seed();
}
// Db sets
}
And
public class DbInitializer
{
private readonly ModelBuilder modelBuilder;
public DbInitializer(ModelBuilder modelBuilder)
{
this.modelBuilder = modelBuilder;
}
public void Seed()
{
modelBuilder.Entity<User>().HasData(
new User(){ Id = 1.... },
new User(){ Id = 2.... },
);
}
}
Then run the command
dotnet ef migrations add .......
To create migration file
And
dotnet ef database update
To update db
.NET 6.0
Use these links for detailed version:
Tutorial: Get started with EF Core in an ASP.NET Core MVC web app
AspNetCore.Docs
Program.cs
var builder = WebApplication.CreateBuilder(args);
//Add services to the container.
builder.Services.AddDbContext<YourDbContext>(
optionsBuilder => optionsBuilder.UseSqlServer("Your connection string goes here") //install - Microsoft.EntityFrameworkCore.SqlServer to use ".UseSqlServer" extension method
builder.Services.AddScoped<DbInitializer>();
var app = builder.Build();
//Configure the HTTP-request pipeline
if (app.Environment.IsDevelopment())
{
app.UseItToSeedSqlServer(); //custom extension method to seed the DB
//configure other services
}
app.Run();
DbInitializerExtension.cs
internal static class DbInitializerExtension
{
public static IApplicationBuilder UseItToSeedSqlServer(this IApplicationBuilder app)
{
ArgumentNullException.ThrowIfNull(app, nameof(app));
using var scope = app.ApplicationServices.CreateScope();
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<YourDbContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
}
return app;
}
}
DbInitializer.cs
internal class DbInitializer
{
internal static void Initialize(YourDbContext dbContext)
{
ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext));
dbContext.Database.EnsureCreated();
if (dbContext.Users.Any()) return;
var users = new User[]
{
new User{ Id = 1, Name = "Bruce Wayne" }
//add other users
};
foreach(var user in users)
dbContext.Users.Add(user);
dbContext.SaveChanges();
}
}
In my case, I'm using Microsoft.AspNetCore.Identity and needed initialize application with default values in database using seed methods.
builder.Services.AddScoped<UserManager<ApplicationUser>>();
builder.Services.AddScoped<RoleManager<IdentityRole>>();
And after line containing
var app = builder.Build();
I have called the seeds methods:
using (var scope = app.Services.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<Usuario>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
await DefaultRoles.SeedAsync(roleManager);
await DefaultAdmin.SeedAsync(userManager);
}
Seeder.cs
public static class Seeder
{
public static void Initialize(DatabaseContext context)
{
context.Database.EnsureCreated();
//your seeding data here
context.SaveChanges();
}
}
Program.cs
var app = builder.Build();
SeedDatabase();
void SeedDatabase()
using(var scope = app.Services.CreateScope())
try{
var scopedContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
Seeder.Initialize(scopedContext);
}
catch{
throw;
}
as simple as it gets before using DI.
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();
}
}
I have read Design-time DbContext Creation that there are 3 ways the EF Core tools (for example, the migration commands) obtain derived DbContext instance from the application at design time as opposed to at run time.
From application service provider
From any parameterless ctor
From a class implementing IDesignTimeDbContextFactory<T>
Here I am only interested in the first method by mimicking the pattern used in Asp.net Core. This code does not compile because I have no idea how to make EF Core tool obtain TheContext instance.
Minimal Working Example
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
public class TheContext : DbContext
{
public TheContext(DbContextOptions<TheContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
private static readonly IConfiguration _configuration;
private static readonly string _connectionString;
static Program()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();
_connectionString = _configuration.GetConnectionString("SqlServer");
}
static void ConfigureServices(IServiceCollection isc)
{
isc.AddSingleton(_ => _configuration);
isc.AddDbContextPool<TheContext>(options => options
.UseSqlServer(_connectionString));
isc.AddSingleton<TheApp>();
}
static void Main()
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
IServiceProvider isp = isc.BuildServiceProvider();
isp.GetService<TheApp>().Run();
}
}
class TheApp
{
readonly TheContext _theContext;
public TheApp(TheContext theContext) => _theContext = theContext;
public void Run()
{
// Do something on _theContext
}
}
Question
How to make EF Core tools obtain DbContext instance from service provider of a console application?
Edit:
I forgot to mention the appsettings.json as follows:
{
"ConnectionStrings": {
"Sqlite": "Data Source=MyDatabase.db",
"SqlServer": "Server=(localdb)\\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True"
}
}
Although the documentation topic is called From application services, it starts with
If your startup project is an ASP.NET Core app, the tools try to obtain the DbContext object from the application's service provider.
Looks like they don't expect project types other than ASP.NET Core app to use application service provider :)
Then it continues with
The tools first try to obtain the service provider by invoking Program.BuildWebHost() and accessing the IWebHost.Services property.
and example:
public static IWebHost BuildWebHost(string[] args) => ...
And here is the trick which works with the current (EF Core 2.1.3) bits. The tools actually are searching the class containing your entry point (usually Program) for a static (does not need to be public) method called BuildWebHost with string[] args parameters, and the important undocumented part - the return type does not need to be IWebHost! It could be any object having public property like this
public IServiceProvider Services { get; }
Which gives us the following solution:
class Program
{
// ...
// Helper method for both Main and BuildWebHost
static IServiceProvider BuildServiceProvider(string[] args)
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
return isc.BuildServiceProvider();
}
static void Main(string[] args)
{
BuildServiceProvider(args).GetService<TheApp>().Run();
}
// This "WebHost" will be used by EF Core design time tools :)
static object BuildWebHost(string[] args) =>
new { Services = BuildServiceProvider(args) };
}
Update: Starting from v2.1, you could also utilize the new CreateWebHostBuilder pattern, but IMHO it just adds another level of complexity not needed here (the previous pattern is still supported). It's similar, but now we need a method called CreateWebHostBuilder which returns an object having public method Build() returning object having public Services property returning IServiceProvider. In order to be reused from Main, we can't use anonymous type and have to create 2 classes, and also this makes it's usage from Main more verbose:
class AppServiceBuilder
{
public ServiceCollection Services { get; } = new ServiceCollection();
public AppServiceProvider Build() => new AppServiceProvider(Services.BuildServiceProvider());
}
class AppServiceProvider
{
public AppServiceProvider(IServiceProvider services) { Services = services; }
public IServiceProvider Services { get; }
}
class Program
{
// ...
static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Services.GetService<TheApp>().Run();
}
// This "WebHostBuilder" will be used by EF Core design time tools :)
static AppServiceBuilder CreateWebHostBuilder(string[] args)
{
var builder = new AppServiceBuilder();
ConfigureServices(builder.Services);
return builder;
}
}
Try this, i've added some changes to make it run :
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.FileExtensions;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
namespace IOCEFCore
{
public class TheContext : DbContext
{
public TheContext(DbContextOptions<TheContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
private static readonly IConfigurationRoot _configuration;
private static readonly string _connectionString;
static Program()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).Build();
}
static void ConfigureServices(IServiceCollection isc)
{
isc.AddSingleton(_ => _configuration);
isc.AddDbContextPool<TheContext>(options => options.UseInMemoryDatabase("myContext"));
isc.AddSingleton<TheApp>();
}
static void Main()
{
IServiceCollection isc = new ServiceCollection();
ConfigureServices(isc);
IServiceProvider isp = isc.BuildServiceProvider();
isp.GetService<TheApp>().Run();
Console.ReadLine();
}
class TheApp
{
readonly TheContext _theContext;
public TheApp(TheContext theContext) => _theContext = theContext;
public void Run()
{
// Do something on _theContext
_theContext.Users.Add(new User {Id = 1, Name = "Me"});
_theContext.SaveChanges();
foreach (var u in _theContext.Users)
{
Console.WriteLine("{0} : {1}", u.Id, u.Name);
}
}
}
}
}
Now it can be easily done with generic hosts.
public class Product
{
public int Id { get; set; }
public string Description { get; set; } = default!;
public override string ToString() => $"Id: {Id}, Description: {Description}";
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Product>().HasData(new[]
{
new Product{Id=1, Description="Sql Server"},
new Product{Id=2, Description="Asp.Net Core"},
new Product{Id=3, Description=".NET MAUI"}
});
}
}
public class Application : IHostedService
{
private readonly AppDbContext context;
public Application(AppDbContext context)
{
this.context = context;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
foreach (var p in await context.Products.ToArrayAsync())
Console.WriteLine(p);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
public class Program
{
public static async Task Main()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureHostConfiguration(icb =>
{
// icb.AddUserSecrets<Program>();
});
builder.ConfigureServices((hbc, isc) =>
{
isc.AddDbContext<AppDbContext>(dcob =>
{
//var constr = hbc.Configuration.GetConnectionString("DefaultConnection");
var constr = "DefaultConnection";
dcob.UseSqlServer(constr);
}, ServiceLifetime.Singleton);
isc.AddHostedService<Application>();
});
//await builder.Build().RunAsync();
await builder.RunConsoleAsync();
}
}
The connection string is retrieved from appsettings.json or secret.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=EFCoreConsoleDb;Integrated Security=true;TrustServerCertificate=true"
}
}
I use a dedicated .Net Core 2.1 library to handle EF Core migrations. It all works well with the cli. I now want my ASPNET Core 2.1 WebApi to automatically migrate at startup. My WebApi references my Migration library.
Here is the interface and its implementation that I declare in my Migration library.
public class Migrator : IMigrator
{
private readonly DbContextOptions _options;
public Migrator(DbContextOptions options) => _options = options;
public void Migrate()
{
using (var context = new MyContext(_options))
context.Database.Migrate();
}
}
public interface IMigrator
{
void Migrate();
}
Here is my WebApi Program.cs (inspired from that repo)
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
try
{
scope.ServiceProvider.GetService<IMigrator>().Migrate();
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while migrating the database.");
}
}
host.Run();
}
private static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
And here in my startup.cs
services.AddScoped<IMigrator, Migrator>(_ => new Migrator(new DbContextOptionsBuilder<DbContext>()
.UseSqlServer(Configuration.GetConnectionString("Write"))
.Options));
It all builds and run an no exceptions are caught. However, my latest migration is not applied to the database.
UPDATE:
The accepted aswer led me to:
services.AddScoped<IMigrator, Migrator>(_ => new Migrator(Configuration.GetConnectionString("Write")));
and
public class Migrator : IMigrator
{
private readonly string _connectionString;
public Migrator(string connectionString) => _connectionString = connectionString;
public void Migrate()
{
using (var context = GetContext(_connectionString))
context.Database.Migrate();
}
private static MyContext GetContext(string connectionString)
{
var builder = new DbContextOptionsBuilder<GtaXContext>();
builder.UseSqlServer(connectionString, b => b.MigrationsAssembly("MyApp.Context.Migrations"));
return new MyContext (builder.Options);
}
}
By default EF Core migrator assumes that the assembly containing the migrations is the one which contains the target DbContext (typeof(MyContext).Assembly), which apparently doesn't apply in your case.
So you need to specify it explicitly via MigrationsAssembly method:
Configures the assembly where migrations are maintained for this context.
For instance:
.UseSqlServer(Configuration.GetConnectionString("Write"), options => options
.MigrationAssembly(typeof(Migrator).Assembly.FullName))
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.