Bugfix/19601 can not add ef core migrations (#19846)

* fix EFCore add migration issue

* update test

* Resolved breaking changes and code review comments.

* Removed extra line break.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Dirk Seefeld
2025-08-04 12:56:25 +02:00
committed by GitHub
parent 7c9c7337b9
commit 4bf2fbf1ba
4 changed files with 80 additions and 33 deletions

View File

@@ -19,7 +19,7 @@ public class UmbracoEFCoreComposer : IComposer
builder.AddNotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification, EFCoreCreateTablesNotificationHandler>(); builder.AddNotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification, EFCoreCreateTablesNotificationHandler>();
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>(); builder.AddNotificationAsyncHandler<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>();
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((options) => builder.Services.AddUmbracoDbContext<UmbracoDbContext>((provider, options, connectionString, providerName) =>
{ {
// Register the entity sets needed by OpenIddict. // Register the entity sets needed by OpenIddict.
options.UseOpenIddict(); options.UseOpenIddict();

View File

@@ -17,32 +17,50 @@ public static class UmbracoEFCoreServiceCollectionExtensions
/// <summary> /// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> [Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
/// <param name="services"></param> public static IServiceCollection AddUmbracoDbContext<T>(
/// <param name="optionsAction"></param> this IServiceCollection services,
/// <returns></returns> Action<DbContextOptionsBuilder>? optionsAction = null)
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null) where T : DbContext
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(optionsBuilder));
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<DbContextOptionsBuilder, string?, string?, IServiceProvider?>? optionsAction = null)
where T : DbContext where T : DbContext
{ {
return AddUmbracoDbContext<T>(services, (IServiceProvider _, DbContextOptionsBuilder options) => return AddUmbracoDbContext<T>(services, (IServiceProvider provider, DbContextOptionsBuilder optionsBuilder, string? providerName, string? connectionString) =>
{ {
optionsAction?.Invoke(options); ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
optionsAction?.Invoke(optionsBuilder, connectionStrings.ConnectionString, connectionStrings.ProviderName, provider);
}); });
} }
/// <summary> /// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> [Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
/// <param name="services"></param> public static IServiceCollection AddUmbracoDbContext<T>(
/// <param name="optionsAction"></param> this IServiceCollection services,
/// <returns></returns> Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null) where T : DbContext
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(sp, optionsBuilder));
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction = null)
where T : DbContext where T : DbContext
{ {
optionsAction ??= (sp, options) => { }; optionsAction ??= (sp, optionsBuilder, connectionString, providerName) => { };
services.AddPooledDbContextFactory<T>(optionsAction);
services.AddPooledDbContextFactory<T>((provider, optionsBuilder) => SetupDbContext(optionsAction, provider, optionsBuilder));
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext()); services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());
services.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>(); services.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>();
@@ -110,4 +128,25 @@ public static class UmbracoEFCoreServiceCollectionExtensions
builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString); builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString);
} }
private static void SetupDbContext(Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction, IServiceProvider provider, DbContextOptionsBuilder builder)
{
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
optionsAction?.Invoke(provider, builder, connectionStrings.ConnectionString, connectionStrings.ProviderName);
}
private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider)
{
ConnectionStrings connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>().CurrentValue;
// Replace data directory
string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString();
if (string.IsNullOrEmpty(dataDirectory) is false)
{
connectionStrings.ConnectionString = connectionStrings.ConnectionString?.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory);
}
return connectionStrings;
}
} }

View File

@@ -1,4 +1,5 @@
using System.Configuration; using System.Configuration;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -17,14 +18,14 @@ namespace Umbraco.Cms.Persistence.EFCore;
/// and insure the 'src/Umbraco.Web.UI/appsettings.json' have a connection string set with the right provider. /// and insure the 'src/Umbraco.Web.UI/appsettings.json' have a connection string set with the right provider.
/// ///
/// Create a migration for each provider. /// Create a migration for each provider.
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext -- --provider SqlServer</code> /// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext</code>
/// ///
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext -- --provider Sqlite</code> /// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext</code>
/// ///
/// Remove the last migration for each provider. /// Remove the last migration for each provider.
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -- --provider SqlServer</code> /// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer</code>
/// ///
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -- --provider Sqlite</code> /// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite</code>
/// ///
/// To find documentation about this way of working with the context see /// To find documentation about this way of working with the context see
/// https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-one-context-type /// https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-one-context-type
@@ -37,28 +38,35 @@ public class UmbracoDbContext : DbContext
/// <param name="options"></param> /// <param name="options"></param>
public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options) public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options)
: base(ConfigureOptions(options)) : base(ConfigureOptions(options))
{ { }
}
private static DbContextOptions<UmbracoDbContext> ConfigureOptions(DbContextOptions<UmbracoDbContext> options) private static DbContextOptions<UmbracoDbContext> ConfigureOptions(DbContextOptions<UmbracoDbContext> options)
{ {
IOptionsMonitor<ConnectionStrings> connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ConnectionStrings>>(); var extensions = options.Extensions.FirstOrDefault() as Microsoft.EntityFrameworkCore.Infrastructure.CoreOptionsExtension;
IServiceProvider? serviceProvider = extensions?.ApplicationServiceProvider;
ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue; serviceProvider ??= StaticServiceProvider.Instance;
if (serviceProvider == null)
if (string.IsNullOrWhiteSpace(connectionStrings.ConnectionString))
{ {
ILogger<UmbracoDbContext> logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<UmbracoDbContext>>(); // If the service provider is null, we cannot resolve the connection string or migration provider.
logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context"); throw new InvalidOperationException("The service provider is not configured. Ensure that UmbracoDbContext is registered correctly.");
}
IOptionsMonitor<ConnectionStrings>? connectionStringsOptionsMonitor = serviceProvider?.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
ConnectionStrings? connectionStrings = connectionStringsOptionsMonitor?.CurrentValue;
if (string.IsNullOrWhiteSpace(connectionStrings?.ConnectionString))
{
ILogger<UmbracoDbContext>? logger = serviceProvider?.GetRequiredService<ILogger<UmbracoDbContext>>();
logger?.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
// we're throwing an exception here to make it abundantly clear that one should never utilize (or have a // we're throwing an exception here to make it abundantly clear that one should never utilize (or have a
// dependency on) the DbContext before the connection string has been initialized by the installer. // dependency on) the DbContext before the connection string has been initialized by the installer.
throw new InvalidOperationException("No connection string was found, cannot setup Umbraco EF Core context"); throw new InvalidOperationException("No connection string was found, cannot setup Umbraco EF Core context");
} }
IEnumerable<IMigrationProviderSetup> migrationProviders = StaticServiceProvider.Instance.GetServices<IMigrationProviderSetup>(); IEnumerable<IMigrationProviderSetup>? migrationProviders = serviceProvider?.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider = migrationProviders.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName)); IMigrationProviderSetup? migrationProvider = migrationProviders?.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
if (migrationProvider == null && connectionStrings.ProviderName != null) if (migrationProvider == null && connectionStrings.ProviderName != null)
{ {

View File

@@ -22,7 +22,7 @@ internal sealed class CustomDbContextUmbracoProviderTests : UmbracoIntegrationTe
protected override void CustomTestSetup(IUmbracoBuilder builder) protected override void CustomTestSetup(IUmbracoBuilder builder)
{ {
builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options) => builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options, connectionString, providerName) =>
{ {
options.UseUmbracoDatabaseProvider(serviceProvider); options.UseUmbracoDatabaseProvider(serviceProvider);
}); });
@@ -53,7 +53,7 @@ public class CustomDbContextCustomSqliteProviderTests : UmbracoIntegrationTest
protected override void CustomTestSetup(IUmbracoBuilder builder) protected override void CustomTestSetup(IUmbracoBuilder builder)
{ {
builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options) => builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options, connectionString, providerName) =>
{ {
options.UseSqlite("Data Source=:memory:;Version=3;New=True;"); options.UseSqlite("Data Source=:memory:;Version=3;New=True;");
}); });