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<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>();
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((options) =>
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((provider, options, connectionString, providerName) =>
{
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();

View File

@@ -17,32 +17,50 @@ public static class UmbracoEFCoreServiceCollectionExtensions
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="services"></param>
/// <param name="optionsAction"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null)
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
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
{
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>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="services"></param>
/// <param name="optionsAction"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
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
{
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.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>();
@@ -110,4 +128,25 @@ public static class UmbracoEFCoreServiceCollectionExtensions
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.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
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.
///
/// 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.
/// <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
/// 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>
public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options)
: base(ConfigureOptions(options))
{
}
{ }
private static DbContextOptions<UmbracoDbContext> ConfigureOptions(DbContextOptions<UmbracoDbContext> options)
{
IOptionsMonitor<ConnectionStrings> connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue;
if (string.IsNullOrWhiteSpace(connectionStrings.ConnectionString))
var extensions = options.Extensions.FirstOrDefault() as Microsoft.EntityFrameworkCore.Infrastructure.CoreOptionsExtension;
IServiceProvider? serviceProvider = extensions?.ApplicationServiceProvider;
serviceProvider ??= StaticServiceProvider.Instance;
if (serviceProvider == null)
{
ILogger<UmbracoDbContext> logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<UmbracoDbContext>>();
logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
// If the service provider is null, we cannot resolve the connection string or migration provider.
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
// 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");
}
IEnumerable<IMigrationProviderSetup> migrationProviders = StaticServiceProvider.Instance.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider = migrationProviders.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
IEnumerable<IMigrationProviderSetup>? migrationProviders = serviceProvider?.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider = migrationProviders?.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
if (migrationProvider == null && connectionStrings.ProviderName != null)
{

View File

@@ -22,7 +22,7 @@ internal sealed class CustomDbContextUmbracoProviderTests : UmbracoIntegrationTe
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options) =>
builder.Services.AddUmbracoDbContext<CustomDbContext>((serviceProvider, options, connectionString, providerName) =>
{
options.UseUmbracoDatabaseProvider(serviceProvider);
});
@@ -53,7 +53,7 @@ public class CustomDbContextCustomSqliteProviderTests : UmbracoIntegrationTest
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;");
});