diff --git a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs index 245bbe5534..08353e8d02 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs @@ -19,7 +19,7 @@ public class UmbracoEFCoreComposer : IComposer builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.Services.AddUmbracoDbContext((options) => + builder.Services.AddUmbracoDbContext((provider, options, connectionString, providerName) => { // Register the entity sets needed by OpenIddict. options.UseOpenIddict(); diff --git a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs index d7da8a65fe..adcfb27406 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs @@ -17,32 +17,50 @@ public static class UmbracoEFCoreServiceCollectionExtensions /// /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. /// - /// - /// - /// - /// - public static IServiceCollection AddUmbracoDbContext(this IServiceCollection services, Action? 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( + this IServiceCollection services, + Action? optionsAction = null) + where T : DbContext + => AddUmbracoDbContext(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(optionsBuilder)); + + /// + /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. + /// + public static IServiceCollection AddUmbracoDbContext( + this IServiceCollection services, + Action? optionsAction = null) where T : DbContext { - return AddUmbracoDbContext(services, (IServiceProvider _, DbContextOptionsBuilder options) => + return AddUmbracoDbContext(services, (IServiceProvider provider, DbContextOptionsBuilder optionsBuilder, string? providerName, string? connectionString) => { - optionsAction?.Invoke(options); + ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider); + optionsAction?.Invoke(optionsBuilder, connectionStrings.ConnectionString, connectionStrings.ProviderName, provider); }); } /// /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. /// - /// - /// - /// - /// - public static IServiceCollection AddUmbracoDbContext(this IServiceCollection services, Action? 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( + this IServiceCollection services, + Action? optionsAction = null) + where T : DbContext + => AddUmbracoDbContext(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(sp, optionsBuilder)); + + /// + /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. + /// + public static IServiceCollection AddUmbracoDbContext( + this IServiceCollection services, + Action? optionsAction = null) where T : DbContext { - optionsAction ??= (sp, options) => { }; + optionsAction ??= (sp, optionsBuilder, connectionString, providerName) => { }; - services.AddPooledDbContextFactory(optionsAction); + + services.AddPooledDbContextFactory((provider, optionsBuilder) => SetupDbContext(optionsAction, provider, optionsBuilder)); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); services.AddUnique, AmbientEFCoreScopeStack>(); @@ -110,4 +128,25 @@ public static class UmbracoEFCoreServiceCollectionExtensions builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString); } + + private static void SetupDbContext(Action? 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>().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; + } } diff --git a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs index 9fcaccfe37..ca69e31727 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs @@ -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. -/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext -- --provider SqlServer +/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext /// -/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext -- --provider Sqlite +/// dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext /// /// Remove the last migration for each provider. -/// dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -- --provider SqlServer +/// dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer /// -/// dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -- --provider Sqlite +/// dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite /// /// 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 /// public UmbracoDbContext(DbContextOptions options) : base(ConfigureOptions(options)) - { - - } + { } private static DbContextOptions ConfigureOptions(DbContextOptions options) { - IOptionsMonitor connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService>(); - - 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 logger = StaticServiceProvider.Instance.GetRequiredService>(); - 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? connectionStringsOptionsMonitor = serviceProvider?.GetRequiredService>(); + + ConnectionStrings? connectionStrings = connectionStringsOptionsMonitor?.CurrentValue; + + if (string.IsNullOrWhiteSpace(connectionStrings?.ConnectionString)) + { + ILogger? logger = serviceProvider?.GetRequiredService>(); + 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 migrationProviders = StaticServiceProvider.Instance.GetServices(); - IMigrationProviderSetup? migrationProvider = migrationProviders.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName)); + IEnumerable? migrationProviders = serviceProvider?.GetServices(); + IMigrationProviderSetup? migrationProvider = migrationProviders?.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName)); if (migrationProvider == null && connectionStrings.ProviderName != null) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs index 4a7c64ea5c..d934f0286d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs @@ -22,7 +22,7 @@ internal sealed class CustomDbContextUmbracoProviderTests : UmbracoIntegrationTe protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.Services.AddUmbracoDbContext((serviceProvider, options) => + builder.Services.AddUmbracoDbContext((serviceProvider, options, connectionString, providerName) => { options.UseUmbracoDatabaseProvider(serviceProvider); }); @@ -53,7 +53,7 @@ public class CustomDbContextCustomSqliteProviderTests : UmbracoIntegrationTest protected override void CustomTestSetup(IUmbracoBuilder builder) { - builder.Services.AddUmbracoDbContext((serviceProvider, options) => + builder.Services.AddUmbracoDbContext((serviceProvider, options, connectionString, providerName) => { options.UseSqlite("Data Source=:memory:;Version=3;New=True;"); });