C
C#11mo ago
Johannes M

✅ EF Core migrations at design time using Clean Architecture - .NET 7

I am currently working on a project using clean architecture with Blazor Server. I have my connection string inside the web project but I would like to create my db migrations inside the infrastructure project where my DbContext is sitting. I have a method that will run any outstanding migrations on project startup but when I want to manually create my migrations or update my database using add-migration or update-database. Each time I try this I get an error that it cannot create my DbContext and I should look at the design time on the MS docs. My way around this was creating a configbuilder inside of the infra project and adding a new appsettings.json file and it worked, Is there a way in which I don't have to create a config builder inside my context factory referencing an appsettings.json file inside of the infra project? When the project runs I am able to inject the connection string via the DI container and IOptions but this does not work when the project is not running.
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Development.json")
.Build();

var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).GetTypeInfo().Assembly.GetName().Name));
return new ApplicationDbContext(builder.Options);
}
}
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Development.json")
.Build();

var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
optionsBuilder => optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).GetTypeInfo().Assembly.GetName().Name));
return new ApplicationDbContext(builder.Options);
}
}
19 Replies
Tarcisio
Tarcisio11mo ago
Why are you using this factory at all? Also, what is the error message and what have you tried?
Johannes M
Johannes M11mo ago
When I had a look at the microsoft docs it recommended using this particular factory if I wanted to run migrations and when implementing it like it seems to work but I don't trust that this will work well when doing a deployment. Error: Unable to create an object of type 'ApplicationDbContext'. For the different patterns supported at design time
Tarcisio
Tarcisio11mo ago
You can simply do await _context.Database.MigrateAsync() You don't need a factory for that Just create a scope and use GetRequiredService or use this if you feel like using it: https://github.com/thomaslevesque/Extensions.Hosting.AsyncInitialization
Johannes M
Johannes M11mo ago
I have that statement in my program.cs but when I add a new entity and try creating a migration for it, that's when it fails at times even if I have my web project as the startup project and have the EF Core Tools and EF Core Design packages installed
Tarcisio
Tarcisio11mo ago
Can you show your Program.cs
Johannes M
Johannes M11mo ago
I have an extensions class
public static class InitialiserExtensions
{
public static async Task InitialiseDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();

var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();

await initialiser.InitialiseAsync();
}
}

public class ApplicationDbContextInitialiser
{
private readonly ApplicationDbContext _context;
private readonly ILogger<ApplicationDbContextInitialiser> _logger;

public ApplicationDbContextInitialiser(ApplicationDbContext context, ILogger<ApplicationDbContextInitialiser> logger)
{
_context = context;
_logger = logger;
}

public async Task InitialiseAsync()
{
try
{
await _context.Database.MigrateAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while initialising the database");
throw;
}
}
}
public static class InitialiserExtensions
{
public static async Task InitialiseDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();

var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();

await initialiser.InitialiseAsync();
}
}

public class ApplicationDbContextInitialiser
{
private readonly ApplicationDbContext _context;
private readonly ILogger<ApplicationDbContextInitialiser> _logger;

public ApplicationDbContextInitialiser(ApplicationDbContext context, ILogger<ApplicationDbContextInitialiser> logger)
{
_context = context;
_logger = logger;
}

public async Task InitialiseAsync()
{
try
{
await _context.Database.MigrateAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while initialising the database");
throw;
}
}
}
in my infra DI container
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
services.AddScoped<ApplicationDbContextInitialiser>();
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
services.AddScoped<ApplicationDbContextInitialiser>();
then in my program.cs
if (app.Environment.IsDevelopment())
{
using (var scope = app.Services.CreateScope())
{
try
{
app.UseMigrationsEndPoint();
await app.InitialiseDatabaseAsync();
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred during database initialization.");

throw;
}
}
}
if (app.Environment.IsDevelopment())
{
using (var scope = app.Services.CreateScope())
{
try
{
app.UseMigrationsEndPoint();
await app.InitialiseDatabaseAsync();
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred during database initialization.");

throw;
}
}
}
Tarcisio
Tarcisio11mo ago
This should have worked, and what exception are you getting?
Johannes M
Johannes M11mo ago
Was not getting any exception except for the one when trying to create a migration in the terminal. But for some odd reason when I added the context factory and added the configuration builder in the context I was able to create the migrations and update the database. Before using the design factory I was using a normal db factory and this was not working
public class BlazorContextFactory<TContext> : IDbContextFactory<TContext> where TContext : DbContext
{
private readonly IServiceProvider _provider;

public BlazorContextFactory(IServiceProvider provider)
{
this._provider = provider;
}

public TContext CreateDbContext()
{
if (_provider == null)
{
throw new InvalidOperationException(
$"You must configure an instance of IServiceProvider");
}

return ActivatorUtilities.CreateInstance<TContext>(_provider);
}
}
public class BlazorContextFactory<TContext> : IDbContextFactory<TContext> where TContext : DbContext
{
private readonly IServiceProvider _provider;

public BlazorContextFactory(IServiceProvider provider)
{
this._provider = provider;
}

public TContext CreateDbContext()
{
if (_provider == null)
{
throw new InvalidOperationException(
$"You must configure an instance of IServiceProvider");
}

return ActivatorUtilities.CreateInstance<TContext>(_provider);
}
}
Tarcisio
Tarcisio11mo ago
use dotnet ef in the CLI, as for the exception you were getting, did you have Microsoft.EntityFrameworkCore.Design in the Startup project?
Johannes M
Johannes M11mo ago
Yeah that was the first error I was getting then installed EF Core Design and then I got the error that it could not create object of type ApplicationDbContext
Tarcisio
Tarcisio11mo ago
Can you try it again with dotnet ef and also show the command you're using
Johannes M
Johannes M11mo ago
Command: dotnet ef migrations add TryingANewOne Exception:
Build started...
Build succeeded.
Unable to create an object of type 'ApplicationDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
Build started...
Build succeeded.
Unable to create an object of type 'ApplicationDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
Tarcisio
Tarcisio11mo ago
pass an --startup-project dotnet ef migrations add {migration name} --project {Where your DbContext is} -o {Migrations FOlder} --startup-project {Your Api Project that has the .Design installed} Are you using visual studio? rider? vscode You can install an extension/plugin that offers a GUI for dotnet ef It's much simpler
Johannes M
Johannes M11mo ago
When using Visual Studio I normally go for the package manager console which then allows me to just run add-migration {migration name}
Tarcisio
Tarcisio11mo ago
Well I use the GUI on Rider, I'm not sure what is the extension name for visual studio but this command I sent should work
Johannes M
Johannes M11mo ago
@tarcisioo That worked. Thank you so much but now does that mean I have to run this long long command each time I want to create a migration? @tarcisioo Even add-migration TryingToAddOne just worked now. This is really weird
Tarcisio
Tarcisio11mo ago
Probably has something to do with the startup project and the dbcontext project in visual studio but nice gui>>cli
Johannes M
Johannes M11mo ago
Thank you so much @tarcisioo 🙏🏽
Accord
Accord11mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.