From 576e497182e8c226d9a5b7ace9548547009ee9a1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 29 May 2025 23:32:35 +0200 Subject: [PATCH] Pooling. (#1225) * Pooling. * Potential fix. --- .../Actions/Script/ScriptFlowStep.cs | 2 +- .../Validation/CompositeUniqueValidator.cs | 2 - .../ContentDbContext.cs | 9 +-- .../Apps/Entities/Contents/DynamicTables.cs | 3 +- .../DelegatingDbNamedContextFactory.cs | 23 -------- .../Infrastructure/Extensions.cs | 31 ++++++++++- .../PooledDbNamedContextFactory.cs | 55 +++++++++++++++++++ .../Infrastructure/PrefixExtension.cs | 44 +++++++++++++++ .../Providers/MySql/App/MySqlAppDbContext.cs | 7 --- .../MySql/Content/MySqlContentDbContext.cs | 26 +-------- .../MySqlContentDbContextDesignTimeFactory.cs | 12 +++- .../Postgres/App/PostgresAppDbContext.cs | 7 --- .../Content/PostgresContentDbContext.cs | 20 +------ ...stgresContentDbContextDesignTimeFactory.cs | 12 +++- .../SqlServer/App/SqlServerAppDbContext.cs | 7 --- .../Content/SqlServerContentDbContext.cs | 13 +---- ...ServerContentDbContextDesignTimeFactory.cs | 12 +++- .../ServiceExtensions.cs | 50 ++++++++++++----- .../Contents/IContentQueryService.cs | 2 +- .../Contents/Queries/ContentLoader.cs | 1 - .../DefaultUserService.cs | 2 +- .../TestHelpers/MySqlFixture.cs | 14 +++-- .../TestHelpers/PostgresFixture.cs | 11 ++-- .../TestHelpers/SqlServerFixture.cs | 11 ++-- .../TestHelpers/TestDbContext.cs | 21 ------- .../rules/pages/rule/rule-page.component.scss | 6 +- .../pages/rule/step-dialog.component.html | 2 + frontend/src/app/shared/state/rules.forms.ts | 2 +- 28 files changed, 241 insertions(+), 166 deletions(-) delete mode 100644 backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs create mode 100644 backend/src/Squidex.Data.EntityFramework/Infrastructure/PrefixExtension.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs index 783d5a4d5..68101d18d 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs @@ -16,7 +16,7 @@ namespace Squidex.Extensions.Actions.Script; [FlowStep( Title = "Script", - IconImage = "", + IconImage = "", IconColor = "#3389ff", Display = "Execute a script", Description = "Execute custom code in Javascript.")] diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs index 8df1f471e..f9de5e4b1 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs @@ -35,7 +35,6 @@ internal sealed class CompositeUniqueValidator(string contentTag, IContentReposi foreach (var field in validateableFields) { var fieldValue = TryGetValue(field, data); - if (fieldValue != null) { filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue)); @@ -47,7 +46,6 @@ internal sealed class CompositeUniqueValidator(string contentTag, IContentReposi var filter = ClrFilter.And(filters); var found = await contentRepository.QueryIdsAsync(context.Root.App, context.Root.Schema, filter, SearchScope.All); - if (found.Any(x => x.Id != context.Root.ContentId)) { context.AddError(Enumerable.Empty(), "A content with the same values already exist."); diff --git a/backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs index e6137ccd9..85a9d9166 100644 --- a/backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs @@ -10,17 +10,18 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; +#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. + namespace Squidex; -public abstract class ContentDbContext(string prefix, IJsonSerializer jsonSerializer) : DbContext, IDbContextWithDialect +public abstract class ContentDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) + : DbContext(options), IDbContextWithDialect { - public string Prefix { get; } = prefix; - public abstract SqlDialect Dialect { get; } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.UseContent(jsonSerializer, Dialect.JsonColumnType(), Prefix); + modelBuilder.UseContent(jsonSerializer, Dialect.JsonColumnType(), options.Prefix()); base.OnModelCreating(modelBuilder); } diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs index 171ca765a..f13ede968 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs @@ -77,11 +77,12 @@ public sealed class DynamicTables( { var prefix = await GetPrefixAsync(name); - using var dbContext = await dbContextNamedFactory.CreateDbContextAsync(prefix, default); + await using var dbContext = await dbContextNamedFactory.CreateDbContextAsync(prefix, default); // Make the prefix available as async local variable because migrations cannot use it otherwise. TableName.Prefix = prefix; await dbContext.Database.MigrateAsync(default); + await dbContext.DisposeAsync(); return prefix; } diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs deleted file mode 100644 index f2ef9fb67..000000000 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.EntityFrameworkCore; -using Squidex.Infrastructure.Json; - -namespace Squidex.Infrastructure; - -public sealed class DelegatingDbNamedContextFactory( - IJsonSerializer jsonSerializer, - Func factory) - : IDbContextNamedFactory where TContext : DbContext -{ - public Task CreateDbContextAsync(string name, - CancellationToken ct = default) - { - return Task.FromResult(factory(jsonSerializer, name)); - } -} diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs index c9141f5aa..3e432e3eb 100644 --- a/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs @@ -6,14 +6,16 @@ // ========================================================================== using System.Linq.Expressions; +using Google.Protobuf; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using PhenX.EntityFrameworkCore.BulkInsert.Extensions; using PhenX.EntityFrameworkCore.BulkInsert.Options; using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.States; @@ -22,15 +24,38 @@ namespace Squidex.Infrastructure; public static class Extensions { public static IServiceCollection AddNamedDbContext(this IServiceCollection services, - Func factory) + Action, string> configure) where TContext : DbContext { services.AddSingleton>(c => - ActivatorUtilities.CreateInstance>(c, factory)); + ActivatorUtilities.CreateInstance>(c, configure)); return services; } + public static DbContextOptionsBuilder UsePrefix(this DbContextOptionsBuilder builder, string prefix) + where TContext : DbContext + { + ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(new PrefixExtension(prefix)); + return builder; + } + + public static DbContextOptionsBuilder UsePoolSize(this DbContextOptionsBuilder builder, int poolSize) + where TContext : DbContext + { + var extension = + (builder.Options.FindExtension() ?? new CoreOptionsExtension()) + .WithMaxPoolSize(poolSize); + + ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension); + return builder; + } + + public static string Prefix(this DbContextOptions options) + { + return options.GetExtension().Prefix; + } + public static DbContextOptionsBuilder SetDefaultWarnings(this DbContextOptionsBuilder builder) { builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer)); diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs new file mode 100644 index 000000000..63f856588 --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; + +#pragma warning disable EF1001 // Internal EF Core API usage. + +namespace Squidex.Infrastructure; + +public sealed class PooledDbNamedContextFactory( + IServiceProvider serviceProvider, + Action, string> configure) + : IDbContextNamedFactory, IAsyncDisposable where TContext : DbContext +{ + private readonly ConcurrentDictionary> pools = new (); + + public async ValueTask DisposeAsync() + { + foreach (var (_, pool) in pools) + { + await pool.DisposeAsync(); + } + } + + public async Task CreateDbContextAsync(string name, + CancellationToken ct = default) + { + var pool = pools.GetOrAdd(name, CreatePool); + + var lease = new DbContextLease(pool, true); + await lease.Context.SetLeaseAsync(lease, ct); + + return (TContext)lease.Context; + } + + private DbContextPool CreatePool(string name) + { + var builder = new DbContextOptionsBuilder(); + + configure(builder, name); + + builder.UsePoolSize(128); + builder.UsePrefix(name); + + var pool = new DbContextPool(builder.Options, serviceProvider); + return pool; + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Infrastructure/PrefixExtension.cs b/backend/src/Squidex.Data.EntityFramework/Infrastructure/PrefixExtension.cs new file mode 100644 index 000000000..4463815ea --- /dev/null +++ b/backend/src/Squidex.Data.EntityFramework/Infrastructure/PrefixExtension.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +#pragma warning disable EF1001 // Internal EF Core API usage. + +namespace Squidex.Infrastructure; + +public sealed class PrefixExtension(string prefix) : IDbContextOptionsExtension +{ + public DbContextOptionsExtensionInfo Info + => new PrefixExtensionInfo(this); + + public string Prefix { get; } = prefix; + + public void ApplyServices(IServiceCollection services) + { + } + + public void Validate(IDbContextOptions options) + { + } + + private class PrefixExtensionInfo(IDbContextOptionsExtension extension) : DbContextOptionsExtensionInfo(extension) + { + public override int GetServiceProviderHashCode() => 0; + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; + + public override bool IsDatabaseProvider => false; + + public override string LogFragment => "PrefixExtension"; + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + } + } +} diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs index 14d40466b..370381d5a 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.MySql; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -16,10 +15,4 @@ public sealed class MySqlAppDbContext(DbContextOptions options, IJsonSerializer : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => MySqlDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertMySql(); - base.OnConfiguring(optionsBuilder); - } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs index 8071b88a9..8a3456b48 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs @@ -6,35 +6,13 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.MySql; -using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; -#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. - namespace Squidex.Providers.MySql.Content; -public sealed class MySqlContentDbContext(string prefix, string connectionString, string? versionString, IJsonSerializer jsonSerializer) - : ContentDbContext(prefix, jsonSerializer) +public sealed class MySqlContentDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) + : ContentDbContext(options, jsonSerializer) { public override SqlDialect Dialect => MySqlDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - var version = - !string.IsNullOrWhiteSpace(versionString) ? - ServerVersion.Parse(versionString) : - ServerVersion.AutoDetect(connectionString); - - optionsBuilder.SetDefaultWarnings(); - optionsBuilder.UseBulkInsertMySql(); - optionsBuilder.UseMySql(connectionString, version, options => - { - options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); - options.MigrationsHistoryTable($"{prefix}MigrationHistory"); - }); - - base.OnConfiguring(optionsBuilder); - } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs index 2040fb958..b8985cedf 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs @@ -6,8 +6,11 @@ // ========================================================================== using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.System; +using Squidex.Providers.MySql.App; namespace Squidex.Providers.MySql.Content; @@ -17,6 +20,13 @@ public sealed class MySqlContentDbContextDesignTimeFactory : IDesignTimeDbContex { const string ConnectionString = "Server=localhost;Port=33060;Database=test;User=mysql;Password=mysql"; - return new MySqlContentDbContext(string.Empty, ConnectionString, null, new SystemJsonSerializer(JsonSerializerOptions.Default)); + var builder = new DbContextOptionsBuilder() + .UsePrefix(string.Empty) + .UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString), options => + { + options.UseNetTopologySuite(); + }); + + return new MySqlContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs index 9f25c5cee..9a8fd9e8d 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -16,10 +15,4 @@ public class PostgresAppDbContext(DbContextOptions options, IJsonSerializer json : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => PostgresDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertPostgreSql(); - base.OnConfiguring(optionsBuilder); - } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs index 01715bd80..8fe278b85 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs @@ -6,29 +6,13 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; -using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; -#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. - namespace Squidex.Providers.Postgres.Content; -public sealed class PostgresContentDbContext(string prefix, string connectionString, IJsonSerializer jsonSerializer) - : ContentDbContext(prefix, jsonSerializer) +public sealed class PostgresContentDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) + : ContentDbContext(options, jsonSerializer) { public override SqlDialect Dialect => PostgresDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertPostgreSql(); - optionsBuilder.SetDefaultWarnings(); - optionsBuilder.UseNpgsql(connectionString, options => - { - options.MigrationsHistoryTable($"{prefix}MigrationHistory"); - }); - - base.OnConfiguring(optionsBuilder); - } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs index 7efb90ed2..c15a86424 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs @@ -6,8 +6,11 @@ // ========================================================================== using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.System; +using Squidex.Providers.Postgres.App; namespace Squidex.Providers.Postgres.Content; @@ -17,6 +20,13 @@ public sealed class PostgresContentDbContextDesignTimeFactory : IDesignTimeDbCon { const string ConnectionString = "Server=localhost;Port=54320;Database=test;User=postgres;Password=postgres"; - return new PostgresContentDbContext(string.Empty, ConnectionString, new SystemJsonSerializer(JsonSerializerOptions.Default)); + var builder = new DbContextOptionsBuilder() + .UsePrefix(string.Empty) + .UseNpgsql(ConnectionString, options => + { + options.UseNetTopologySuite(); + }); + + return new PostgresContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs index 6366dd279..5b69cfcb2 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs @@ -6,7 +6,6 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -16,10 +15,4 @@ public sealed class SqlServerAppDbContext(DbContextOptions options, IJsonSeriali : AppDbContext(options, jsonSerializer) { public override SqlDialect Dialect => SqlServerDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertSqlServer(); - base.OnConfiguring(optionsBuilder); - } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs index aea3bd742..d0c4f1f71 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs @@ -6,8 +6,6 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; -using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -15,20 +13,13 @@ using Squidex.Infrastructure.Queries; namespace Squidex.Providers.SqlServer.Content; -public sealed class SqlServerContentDbContext(string prefix, string connectionString, IJsonSerializer jsonSerializer) - : ContentDbContext(prefix, jsonSerializer) +public sealed class SqlServerContentDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) + : ContentDbContext(options, jsonSerializer) { public override SqlDialect Dialect => SqlServerDialect.Instance; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.SetDefaultWarnings(); - optionsBuilder.UseBulkInsertSqlServer(); - optionsBuilder.UseSqlServer(connectionString, options => - { - options.MigrationsHistoryTable($"{prefix}MigrationHistory"); - }); - base.OnConfiguring(optionsBuilder); } } diff --git a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs index 07dc381ff..31c031479 100644 --- a/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs +++ b/backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs @@ -6,8 +6,11 @@ // ========================================================================== using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.System; +using Squidex.Providers.SqlServer.App; namespace Squidex.Providers.SqlServer.Content; @@ -17,6 +20,13 @@ public sealed class SqlServerContentDbContextDesignTimeFactory : IDesignTimeDbCo { const string ConnectionString = "Server=localhost;Port=14330;Database=test;User=sa;Password=sqlserver"; - return new SqlServerContentDbContext(string.Empty, ConnectionString, new SystemJsonSerializer(JsonSerializerOptions.Default)); + var builder = new DbContextOptionsBuilder() + .UsePrefix(string.Empty) + .UseSqlServer(ConnectionString, options => + { + options.UseNetTopologySuite(); + }); + + return new SqlServerContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default)); } } diff --git a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs index a2c531705..0d130b2f7 100644 --- a/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs +++ b/backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs @@ -10,6 +10,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Assets.TusAdapter; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Assets; @@ -70,24 +73,31 @@ public static class ServiceExtensions { var versionString = config.GetOptionalValue("store:sql:version"); - services.AddDbContextFactory(builder => - { - var version = - !string.IsNullOrWhiteSpace(versionString) ? - ServerVersion.Parse(versionString) : - ServerVersion.AutoDetect(connectionString); + var version = + !string.IsNullOrWhiteSpace(versionString) ? + ServerVersion.Parse(versionString) : + ServerVersion.AutoDetect(connectionString); + services.AddPooledDbContextFactory(builder => + { builder.SetDefaultWarnings(); builder.UseMySql(connectionString, version, options => { options.UseNetTopologySuite(); options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); }); + builder.UseBulkInsertMySql(); }); - services.AddNamedDbContext((jsonSerializer, name) => + services.AddNamedDbContext((builder, name) => { - return new MySqlContentDbContext(name, connectionString, versionString, jsonSerializer); + builder.SetDefaultWarnings(); + builder.UseBulkInsertMySql(); + builder.UseMySql(connectionString, version, options => + { + options.MigrationsHistoryTable($"{name}MigrationHistory"); + options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); + }); }); services.AddSingleton(typeof(ISnapshotStore<>), typeof(MySqlSnapshotStore<>)); @@ -101,18 +111,24 @@ public static class ServiceExtensions }, ["Postgres"] = () => { - services.AddDbContextFactory(builder => + services.AddPooledDbContextFactory(builder => { builder.SetDefaultWarnings(); + builder.UseBulkInsertPostgreSql(); builder.UseNpgsql(connectionString, options => { options.UseNetTopologySuite(); }); }); - services.AddNamedDbContext((jsonSerializer, name) => + services.AddNamedDbContext((builder, name) => { - return new PostgresContentDbContext(name, connectionString, jsonSerializer); + builder.SetDefaultWarnings(); + builder.UseBulkInsertPostgreSql(); + builder.UseNpgsql(connectionString, options => + { + options.MigrationsHistoryTable($"{name}MigrationHistory"); + }); }); services.AddSingleton(typeof(ISnapshotStore<>), typeof(PostgresSnapshotStore<>)); @@ -126,18 +142,24 @@ public static class ServiceExtensions }, ["SqlServer"] = () => { - services.AddDbContextFactory(builder => + services.AddPooledDbContextFactory(builder => { builder.SetDefaultWarnings(); builder.UseSqlServer(connectionString, options => { options.UseNetTopologySuite(); }); + builder.UseBulkInsertSqlServer(); }); - services.AddNamedDbContext((jsonSerializer, name) => + services.AddNamedDbContext((builder, name) => { - return new SqlServerContentDbContext(name, connectionString, jsonSerializer); + builder.SetDefaultWarnings(); + builder.UseBulkInsertSqlServer(); + builder.UseSqlServer(connectionString, options => + { + options.MigrationsHistoryTable($"{name}MigrationHistory"); + }); }); services.AddSingleton(typeof(ISnapshotStore<>), typeof(SqlServerSnapshotStore<>)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 0885ac964..f918c3d86 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -27,6 +27,6 @@ public interface IContentQueryService Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName, CancellationToken ct = default); - Task GetSchemaAsync(Context context, string schemaIdOrNama, + Task GetSchemaAsync(Context context, string schemaIdOrName, CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs index bcefc33c0..ad00fb135 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -20,7 +20,6 @@ public sealed class ContentLoader(IDomainObjectFactory domainObjectFactory, IDom var uniqueId = DomainId.Combine(appId, id); var content = await GetCachedAsync(uniqueId, version, ct); - if (content == null) { content = await GetAsync(uniqueId, version, ct); diff --git a/backend/src/Squidex.Domain.Users/DefaultUserService.cs b/backend/src/Squidex.Domain.Users/DefaultUserService.cs index 21bf5a34c..4717c1bcb 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserService.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserService.cs @@ -77,7 +77,7 @@ public sealed class DefaultUserService( return result; } - var userItems = QueryUsers(query).Skip(skip).Take(take).ToList(); + var userItems = QueryUsers(query).OrderBy(x => x.NormalizedEmail).Skip(skip).Take(take).ToList(); var userTotal = QueryUsers(query).LongCount(); var resolved = await ResolveAsync(userItems); diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs index 9503a3570..5d98aa4e9 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; using Squidex.Infrastructure; @@ -42,17 +43,22 @@ public class MySqlFixture(string? reuseId = null) : IAsyncLifetime, ISqlContentF services = new ServiceCollection() - .AddDbContextFactory(b => + .AddPooledDbContextFactory(builder => { - b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options => + builder.UseBulkInsertMySql(); + builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options => { options.UseNetTopologySuite(); options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); }); }) - .AddNamedDbContext((jsonSerializer, name) => + .AddNamedDbContext((builder, name) => { - return new MySqlContentDbContext(name, connectionString, null, jsonSerializer); + builder.UseBulkInsertMySql(); + builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options => + { + options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically); + }); }) .AddSingleton() .AddSingletonAs>().Done() diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs index 0d20e67e5..3ecbd2e99 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; using Squidex.Infrastructure; @@ -42,16 +43,18 @@ public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixtu services = new ServiceCollection() - .AddDbContextFactory(b => + .AddPooledDbContextFactory(builder => { - b.UseNpgsql(connectionString, options => + builder.UseBulkInsertPostgreSql(); + builder.UseNpgsql(connectionString, options => { options.UseNetTopologySuite(); }); }) - .AddNamedDbContext((jsonSerializer, name) => + .AddNamedDbContext((builder, name) => { - return new PostgresContentDbContext(name, connectionString, jsonSerializer); + builder.UseBulkInsertPostgreSql(); + builder.UseNpgsql(connectionString); }) .AddSingleton() .AddSingletonAs>().Done() diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs index 0da509b79..0d5c573c6 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs @@ -8,6 +8,7 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Hosting; using Squidex.Infrastructure; @@ -44,16 +45,18 @@ public class SqlServerFixture(string? reuseId = null) : IAsyncLifetime, ISqlCont services = new ServiceCollection() - .AddDbContextFactory(b => + .AddPooledDbContextFactory(builder => { - b.UseSqlServer(connectionString, options => + builder.UseBulkInsertSqlServer(); + builder.UseSqlServer(connectionString, options => { options.UseNetTopologySuite(); }); }) - .AddNamedDbContext((jsonSerializer, name) => + .AddNamedDbContext((builder, name) => { - return new SqlServerContentDbContext(name, connectionString, jsonSerializer); + builder.UseBulkInsertSqlServer(); + builder.UseSqlServer(connectionString); }) .AddSingleton() .AddSingletonAs>().Done() diff --git a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs index 68edb0512..bde5236bf 100644 --- a/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs +++ b/backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs @@ -6,9 +6,6 @@ // ========================================================================== using Microsoft.EntityFrameworkCore; -using PhenX.EntityFrameworkCore.BulkInsert.MySql; -using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; -using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Queries; @@ -27,36 +24,18 @@ public class TestDbContextMySql(DbContextOptions options, IJsonSerializer jsonSe : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => MySqlDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertMySql(); - base.OnConfiguring(optionsBuilder); - } } public class TestDbContextPostgres(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => PostgresDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertPostgreSql(); - base.OnConfiguring(optionsBuilder); - } } public class TestDbContextSqlServer(DbContextOptions options, IJsonSerializer jsonSerializer) : TestDbContext(options, jsonSerializer) { public override SqlDialect Dialect => SqlServerDialect.Instance; - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseBulkInsertSqlServer(); - base.OnConfiguring(optionsBuilder); - } } public abstract class TestDbContext(DbContextOptions options, IJsonSerializer jsonSerializer) diff --git a/frontend/src/app/features/rules/pages/rule/rule-page.component.scss b/frontend/src/app/features/rules/pages/rule/rule-page.component.scss index 8eed670aa..755f96e78 100644 --- a/frontend/src/app/features/rules/pages/rule/rule-page.component.scss +++ b/frontend/src/app/features/rules/pages/rule/rule-page.component.scss @@ -3,10 +3,7 @@ :host ::ng-deep { .panel2-main-inner.white { - background-image: radial-gradient( - lighten($color-border, 2%) 1px, - transparent 0 - ); + background-image: radial-gradient(lighten($color-border, 2%) 1px, transparent 0); background-size: 1rem 1rem; } @@ -23,6 +20,7 @@ .btn-enabled { @include absolute(1.5rem, 1.75rem, null, null); + background-color: $color-white; &:hover { background-color: $color-white; diff --git a/frontend/src/app/features/rules/pages/rule/step-dialog.component.html b/frontend/src/app/features/rules/pages/rule/step-dialog.component.html index 44e24c05e..d06c662ae 100644 --- a/frontend/src/app/features/rules/pages/rule/step-dialog.component.html +++ b/frontend/src/app/features/rules/pages/rule/step-dialog.component.html @@ -18,6 +18,7 @@ +
{{ "rules.stepIgnoreErrorHint" | sqxTranslate }}
+

diff --git a/frontend/src/app/shared/state/rules.forms.ts b/frontend/src/app/shared/state/rules.forms.ts index 3079873a5..872d47d97 100644 --- a/frontend/src/app/shared/state/rules.forms.ts +++ b/frontend/src/app/shared/state/rules.forms.ts @@ -64,7 +64,7 @@ class BranchTemplate { condition: new UntypedFormControl('', Validators.nullValidator, ), - nextStepId: new UntypedFormControl('', + nextStepId: new UntypedFormControl(undefined, Validators.nullValidator, ), });