diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs index bac08556a3..823a9e737f 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Persistence.EFCore.Migrations; using Umbraco.Extensions; @@ -10,7 +11,7 @@ public class SqlServerMigrationProvider : IMigrationProvider public SqlServerMigrationProvider(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; - public string ProviderName => "Microsoft.Data.SqlClient"; + public string ProviderName => Constants.ProviderNames.SQLServer; public async Task MigrateAsync(EFCoreMigration migration) { diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProviderSetup.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProviderSetup.cs index 6b161fc47f..2d561e9f5a 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProviderSetup.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProviderSetup.cs @@ -1,11 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Persistence.EFCore.Migrations; namespace Umbraco.Cms.Persistence.EFCore.SqlServer; public class SqlServerMigrationProviderSetup : IMigrationProviderSetup { - public string ProviderName => "Microsoft.Data.SqlClient"; + public string ProviderName => Constants.ProviderNames.SQLServer; public void Setup(DbContextOptionsBuilder builder, string? connectionString) { diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs index 05d4024bb3..468f52021f 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Persistence.EFCore.Migrations; using Umbraco.Extensions; @@ -11,7 +12,7 @@ public class SqliteMigrationProvider : IMigrationProvider public SqliteMigrationProvider(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; - public string ProviderName => "Microsoft.Data.Sqlite"; + public string ProviderName => Constants.ProviderNames.SQLLite; public async Task MigrateAsync(EFCoreMigration migration) { diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProviderSetup.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProviderSetup.cs index 4cba457768..3a1b97e76c 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProviderSetup.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProviderSetup.cs @@ -1,11 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Umbraco.Cms.Core; using Umbraco.Cms.Persistence.EFCore.Migrations; namespace Umbraco.Cms.Persistence.EFCore.Sqlite; public class SqliteMigrationProviderSetup : IMigrationProviderSetup { - public string ProviderName => "Microsoft.Data.Sqlite"; + public string ProviderName => Constants.ProviderNames.SQLLite; public void Setup(DbContextOptionsBuilder builder, string? connectionString) { diff --git a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs index 8b8627df47..1b751c558b 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Composition/UmbracoEFCoreComposer.cs @@ -20,7 +20,7 @@ public class UmbracoEFCoreComposer : IComposer builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.Services.AddUmbracoEFCoreContext((options, connectionString, providerName) => + builder.Services.AddUmbracoDbContext((options) => { // 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 ded5be40fd..d901088064 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs @@ -1,7 +1,9 @@ +using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; @@ -16,6 +18,7 @@ public static class UmbracoEFCoreServiceCollectionExtensions { public delegate void DefaultEFCoreOptionsAction(DbContextOptionsBuilder options, string? providerName, string? connectionString); + [Obsolete("Use AddUmbracoDbContext(this IServiceCollection services, Action? optionsAction = null) instead.")] public static IServiceCollection AddUmbracoEFCoreContext(this IServiceCollection services, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null) where T : DbContext { @@ -24,7 +27,7 @@ public static class UmbracoEFCoreServiceCollectionExtensions sp => { SetupDbContext(defaultEFCoreOptionsAction, sp, optionsBuilder); - return new UmbracoPooledDbContextFactory(sp.GetRequiredService(),optionsBuilder.Options); + return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); }); services.AddPooledDbContextFactory((provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder)); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); @@ -38,6 +41,7 @@ public static class UmbracoEFCoreServiceCollectionExtensions return services; } + [Obsolete("Use AddUmbracoDbContext(this IServiceCollection services, Action? optionsAction = null) instead.")] public static IServiceCollection AddUmbracoEFCoreContext(this IServiceCollection services, string connectionString, string providerName, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null) where T : DbContext { @@ -52,8 +56,8 @@ public static class UmbracoEFCoreServiceCollectionExtensions services.TryAddSingleton>( sp => { - SetupDbContext(defaultEFCoreOptionsAction, sp, optionsBuilder); - return new UmbracoPooledDbContextFactory(sp.GetRequiredService(),optionsBuilder.Options); + defaultEFCoreOptionsAction?.Invoke(optionsBuilder, providerName, connectionString); + return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); }); services.AddPooledDbContextFactory(options => defaultEFCoreOptionsAction?.Invoke(options, providerName, connectionString)); services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); @@ -67,12 +71,117 @@ public static class UmbracoEFCoreServiceCollectionExtensions return services; } + /// + /// 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) => + { + optionsAction?.Invoke(options); + }); + } + + /// + /// 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) => { }; + + var optionsBuilder = new DbContextOptionsBuilder(); + + services.TryAddSingleton>(sp => + { + optionsAction.Invoke(sp, optionsBuilder); + return new UmbracoPooledDbContextFactory(sp.GetRequiredService(), optionsBuilder.Options); + }); + services.AddPooledDbContextFactory(optionsAction); + services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); + + services.AddUnique, AmbientEFCoreScopeStack>(); + services.AddUnique, EFCoreScopeAccessor>(); + services.AddUnique, EFCoreScopeProvider>(); + services.AddSingleton>(); + services.AddSingleton>(); + + return services; + } + + /// + /// Sets the database provider. I.E UseSqlite or UseSqlServer based on the provider name. + /// + /// + /// + /// + /// + /// + /// Only supports the databases normally supported in Umbraco. + /// + public static void UseDatabaseProvider(this DbContextOptionsBuilder builder, string providerName, string connectionString) + { + switch (providerName) + { + case Constants.ProviderNames.SQLServer: + builder.UseSqlServer(connectionString); + break; + case Constants.ProviderNames.SQLLite: + builder.UseSqlite(connectionString); + break; + default: + throw new InvalidDataException($"The provider {providerName} is not supported. Manually add the add the UseXXX statement to the options. I.E UseNpgsql()"); + } + } + + /// + /// Sets the database provider to use based on the Umbraco connection string. + /// + /// + /// + public static void UseUmbracoDatabaseProvider(this DbContextOptionsBuilder builder, 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); + } + + if (string.IsNullOrEmpty(connectionStrings.ProviderName)) + { + Log.Warning("No database provider was set. ProviderName is null"); + return; + } + + if (string.IsNullOrEmpty(connectionStrings.ConnectionString)) + { + Log.Warning("No database provider was set. Connection string is null"); + return; + } + + builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString); + } + + [Obsolete] private static void SetupDbContext(DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction, IServiceProvider provider, DbContextOptionsBuilder builder) { ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider); defaultEFCoreOptionsAction?.Invoke(builder, connectionStrings.ConnectionString, connectionStrings.ProviderName); } + [Obsolete] private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider) { ConnectionStrings connectionStrings = serviceProvider.GetRequiredService>().CurrentValue; diff --git a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs index 042cf2a52f..60e519de4c 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Persistence.EFCore.Migrations; @@ -77,7 +76,7 @@ public class UmbracoDbContext : DbContext foreach (IMutableEntityType entity in modelBuilder.Model.GetEntityTypes()) { - entity.SetTableName(Constants.DatabaseSchema.TableNamePrefix + entity.GetTableName()); + entity.SetTableName(Core.Constants.DatabaseSchema.TableNamePrefix + entity.GetTableName()); } } } diff --git a/src/Umbraco.Core/Constants-ProviderNames.cs b/src/Umbraco.Core/Constants-ProviderNames.cs new file mode 100644 index 0000000000..67f376612c --- /dev/null +++ b/src/Umbraco.Core/Constants-ProviderNames.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + public static class ProviderNames + { + public const string SQLLite = "Microsoft.Data.Sqlite"; + + public const string SQLServer = "Microsoft.Data.SqlClient"; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 3099bc561c..1e455aea5f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -35,15 +35,18 @@ $scope.activeApp = null; //initializes any watches - function startWatches(content) { + var watchers = []; - $scope.$watchGroup(['culture', 'segment'], + function startWatches(content) { + clearWatchers(); + + watchers.push($scope.$watchGroup(['culture', 'segment'], function (value, oldValue) { createPreviewButton($scope.content, value[0], value[1]); - }); + })); //watch for changes to isNew, set the page.isNew accordingly and load the breadcrumb if we can - $scope.$watch('isNew', function (newVal, oldVal) { + watchers.push($scope.$watch('isNew', function (newVal, oldVal) { $scope.page.isNew = Object.toBoolean(newVal); @@ -59,8 +62,12 @@ }); } } - }); + })); + } + function clearWatchers () { + watchers.forEach(w => w()); + watchers = []; } //this initializes the editor with the data which will be called more than once if the data is re-loaded @@ -109,6 +116,7 @@ bindEvents(); resetVariantFlags(); + startWatches($scope.content); } function loadBreadcrumb() { @@ -241,7 +249,6 @@ appendRuntimeData(); init(); - startWatches($scope.content); syncTreeNode($scope.content, $scope.content.path, true); @@ -265,7 +272,6 @@ appendRuntimeData(); init(); - startWatches($scope.content); resetLastListPageNumber($scope.content); @@ -346,7 +352,10 @@ labelKey: "buttons_saveAndPreview" }; - const activeVariant = content.variants?.find((variant) => content.documentType?.variations === "Nothing" || variant.compositeId === compositeId); + let activeVariant = content.variants?.find((variant) => content.documentType?.variations === "Nothing" || variant.compositeId === compositeId); + /* if we can't find the active variant and there is only one variant available, we will use that. + this happens if we have a node that can vary by culture but there is only one language available. */ + activeVariant = !activeVariant && content.variants.length === 1 ? content.variants[0] : activeVariant; $scope.previewSubButtons = activeVariant?.additionalPreviewUrls?.map((additionalPreviewUrl) => { return { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextTests.cs new file mode 100644 index 0000000000..bfa3adb92b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextTests.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Persistence.EFCore.DbContext; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] +public class CustomDbContextUmbracoProviderTests : UmbracoIntegrationTest +{ + [Test] + public void Can_Register_Custom_DbContext_And_Resolve() + { + var dbContext = Services.GetRequiredService(); + + Assert.IsNotNull(dbContext); + Assert.IsNotEmpty(dbContext.Database.GetConnectionString()); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUmbracoDbContext((serviceProvider, options) => + { + options.UseUmbracoDatabaseProvider(serviceProvider); + }); + } + + internal class CustomDbContext : Microsoft.EntityFrameworkCore.DbContext + { + public CustomDbContext(DbContextOptions options) + : base(options) + { + } + } +} + + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] +public class CustomDbContextCustomSqliteProviderTests : UmbracoIntegrationTest +{ + [Test] + public void Can_Register_Custom_DbContext_And_Resolve() + { + var dbContext = Services.GetRequiredService(); + + Assert.IsNotNull(dbContext); + Assert.IsNotEmpty(dbContext.Database.GetConnectionString()); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUmbracoDbContext((serviceProvider, options) => + { + options.UseSqlite("Data Source=:memory:;Version=3;New=True;"); + }); + } + + internal class CustomDbContext : Microsoft.EntityFrameworkCore.DbContext + { + public CustomDbContext(DbContextOptions options) + : base(options) + { + } + } +} + +[Obsolete] +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] +public class CustomDbContextLegacyExtensionProviderTests : UmbracoIntegrationTest +{ + [Test] + public void Can_Register_Custom_DbContext_And_Resolve() + { + var dbContext = Services.GetRequiredService(); + + Assert.IsNotNull(dbContext); + Assert.IsNotEmpty(dbContext.Database.GetConnectionString()); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUmbracoEFCoreContext("Data Source=:memory:;Version=3;New=True;", "Microsoft.Data.Sqlite", (options, connectionString, providerName) => + { + options.UseSqlite(connectionString); + }); + } + + internal class CustomDbContext : Microsoft.EntityFrameworkCore.DbContext + { + public CustomDbContext(DbContextOptions options) + : base(options) + { + } + } +} + diff --git a/version.json b/version.json index 9213512058..b830ca0ae6 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.3.0-rc", + "version": "12.3.0", "assemblyVersion": { "precision": "build" },