Browse Source

Pooling. (#1225)

* Pooling.

* Potential fix.
pull/1226/head
Sebastian Stehle 1 year ago
committed by GitHub
parent
commit
576e497182
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs
  2. 2
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs
  3. 9
      backend/src/Squidex.Data.EntityFramework/ContentDbContext.cs
  4. 3
      backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs
  5. 23
      backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs
  6. 31
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Extensions.cs
  7. 55
      backend/src/Squidex.Data.EntityFramework/Infrastructure/PooledDbNamedContextFactory.cs
  8. 44
      backend/src/Squidex.Data.EntityFramework/Infrastructure/PrefixExtension.cs
  9. 7
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/MySqlAppDbContext.cs
  10. 26
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContext.cs
  11. 12
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Content/MySqlContentDbContextDesignTimeFactory.cs
  12. 7
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/PostgresAppDbContext.cs
  13. 20
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContext.cs
  14. 12
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/Content/PostgresContentDbContextDesignTimeFactory.cs
  15. 7
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/SqlServerAppDbContext.cs
  16. 13
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContext.cs
  17. 12
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Content/SqlServerContentDbContextDesignTimeFactory.cs
  18. 50
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  20. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs
  21. 2
      backend/src/Squidex.Domain.Users/DefaultUserService.cs
  22. 14
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs
  23. 11
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs
  24. 11
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs
  25. 21
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/TestDbContext.cs
  26. 6
      frontend/src/app/features/rules/pages/rule/rule-page.component.scss
  27. 2
      frontend/src/app/features/rules/pages/rule/step-dialog.component.html
  28. 2
      frontend/src/app/shared/state/rules.forms.ts

2
backend/extensions/Squidex.Extensions/Actions/Script/ScriptFlowStep.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.Actions.Script;
[FlowStep(
Title = "Script",
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24'><path d='M300-360q-25 0-42.5-17.5T240-420v-40h60v40h60v-180h60v180q0 25-17.5 42.5T360-360h-60zm220 0q-17 0-28.5-11.5T480-400v-40h60v20h80v-40H520q-17 0-28.5-11.5T480-500v-60q0-17 11.5-28.5T520-600h120q17 0 28.5 11.5T680-560v40h-60v-20h-80v40h100q17 0 28.5 11.5T680-460v60q0 17-11.5 28.5T640-360H520z'/></svg>",
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='240 -600 440 240'><path d='M300-360q-25 0-42.5-17.5T240-420v-40h60v40h60v-180h60v180q0 25-17.5 42.5T360-360h-60zm220 0q-17 0-28.5-11.5T480-400v-40h60v20h80v-40H520q-17 0-28.5-11.5T480-500v-60q0-17 11.5-28.5T520-600h120q17 0 28.5 11.5T680-560v40h-60v-20h-80v40h100q17 0 28.5 11.5T680-460v60q0 17-11.5 28.5T640-360H520z'></path></svg>",
IconColor = "#3389ff",
Display = "Execute a script",
Description = "Execute custom code in Javascript.")]

2
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<string>(), "A content with the same values already exist.");

9
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);
}

3
backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/DynamicTables.cs

@ -77,11 +77,12 @@ public sealed class DynamicTables<TContext, TContentContext>(
{
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;
}

23
backend/src/Squidex.Data.EntityFramework/Infrastructure/DelegatingDbNamedContextFactory.cs

@ -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<TContext>(
IJsonSerializer jsonSerializer,
Func<IJsonSerializer, string, TContext> factory)
: IDbContextNamedFactory<TContext> where TContext : DbContext
{
public Task<TContext> CreateDbContextAsync(string name,
CancellationToken ct = default)
{
return Task.FromResult(factory(jsonSerializer, name));
}
}

31
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<TContext>(this IServiceCollection services,
Func<IJsonSerializer, string, TContext> factory)
Action<DbContextOptionsBuilder<TContext>, string> configure)
where TContext : DbContext
{
services.AddSingleton<IDbContextNamedFactory<TContext>>(c =>
ActivatorUtilities.CreateInstance<DelegatingDbNamedContextFactory<TContext>>(c, factory));
ActivatorUtilities.CreateInstance<PooledDbNamedContextFactory<TContext>>(c, configure));
return services;
}
public static DbContextOptionsBuilder<TContext> UsePrefix<TContext>(this DbContextOptionsBuilder<TContext> builder, string prefix)
where TContext : DbContext
{
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(new PrefixExtension(prefix));
return builder;
}
public static DbContextOptionsBuilder<TContext> UsePoolSize<TContext>(this DbContextOptionsBuilder<TContext> builder, int poolSize)
where TContext : DbContext
{
var extension =
(builder.Options.FindExtension<CoreOptionsExtension>() ?? new CoreOptionsExtension())
.WithMaxPoolSize(poolSize);
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension);
return builder;
}
public static string Prefix(this DbContextOptions options)
{
return options.GetExtension<PrefixExtension>().Prefix;
}
public static DbContextOptionsBuilder SetDefaultWarnings(this DbContextOptionsBuilder builder)
{
builder.ConfigureWarnings(w => w.Ignore(CoreEventId.CollectionWithoutComparer));

55
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<TContext>(
IServiceProvider serviceProvider,
Action<DbContextOptionsBuilder<TContext>, string> configure)
: IDbContextNamedFactory<TContext>, IAsyncDisposable where TContext : DbContext
{
private readonly ConcurrentDictionary<string, DbContextPool<TContext>> pools = new ();
public async ValueTask DisposeAsync()
{
foreach (var (_, pool) in pools)
{
await pool.DisposeAsync();
}
}
public async Task<TContext> 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<TContext> CreatePool(string name)
{
var builder = new DbContextOptionsBuilder<TContext>();
configure(builder, name);
builder.UsePoolSize(128);
builder.UsePrefix(name);
var pool = new DbContextPool<TContext>(builder.Options, serviceProvider);
return pool;
}
}

44
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<string, string> debugInfo)
{
}
}
}

7
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);
}
}

26
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);
}
}

12
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<MySqlAppDbContext>()
.UsePrefix(string.Empty)
.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString), options =>
{
options.UseNetTopologySuite();
});
return new MySqlContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

7
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);
}
}

20
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);
}
}

12
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<PostgresAppDbContext>()
.UsePrefix(string.Empty)
.UseNpgsql(ConnectionString, options =>
{
options.UseNetTopologySuite();
});
return new PostgresContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

7
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);
}
}

13
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);
}
}

12
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<SqlServerAppDbContext>()
.UsePrefix(string.Empty)
.UseSqlServer(ConnectionString, options =>
{
options.UseNetTopologySuite();
});
return new SqlServerContentDbContext(builder.Options, new SystemJsonSerializer(JsonSerializerOptions.Default));
}
}

50
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<string>("store:sql:version");
services.AddDbContextFactory<MySqlAppDbContext>(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<MySqlAppDbContext>(builder =>
{
builder.SetDefaultWarnings();
builder.UseMySql(connectionString, version, options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
builder.UseBulkInsertMySql();
});
services.AddNamedDbContext((jsonSerializer, name) =>
services.AddNamedDbContext<MySqlContentDbContext>((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<PostgresAppDbContext>(builder =>
services.AddPooledDbContextFactory<PostgresAppDbContext>(builder =>
{
builder.SetDefaultWarnings();
builder.UseBulkInsertPostgreSql();
builder.UseNpgsql(connectionString, options =>
{
options.UseNetTopologySuite();
});
});
services.AddNamedDbContext((jsonSerializer, name) =>
services.AddNamedDbContext<PostgresContentDbContext>((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<SqlServerAppDbContext>(builder =>
services.AddPooledDbContextFactory<SqlServerAppDbContext>(builder =>
{
builder.SetDefaultWarnings();
builder.UseSqlServer(connectionString, options =>
{
options.UseNetTopologySuite();
});
builder.UseBulkInsertSqlServer();
});
services.AddNamedDbContext((jsonSerializer, name) =>
services.AddNamedDbContext<SqlServerContentDbContext>((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<>));

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -27,6 +27,6 @@ public interface IContentQueryService
Task<Schema> GetSchemaOrThrowAsync(Context context, string schemaIdOrName,
CancellationToken ct = default);
Task<Schema?> GetSchemaAsync(Context context, string schemaIdOrNama,
Task<Schema?> GetSchemaAsync(Context context, string schemaIdOrName,
CancellationToken ct = default);
}

1
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);

2
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);

14
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<TestDbContextMySql>(b =>
.AddPooledDbContextFactory<TestDbContextMySql>(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<MySqlContentDbContext>((builder, name) =>
{
return new MySqlContentDbContext(name, connectionString, null, jsonSerializer);
builder.UseBulkInsertMySql();
builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextMySql>>().Done()

11
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<TestDbContextPostgres>(b =>
.AddPooledDbContextFactory<TestDbContextPostgres>(builder =>
{
b.UseNpgsql(connectionString, options =>
builder.UseBulkInsertPostgreSql();
builder.UseNpgsql(connectionString, options =>
{
options.UseNetTopologySuite();
});
})
.AddNamedDbContext((jsonSerializer, name) =>
.AddNamedDbContext<PostgresContentDbContext>((builder, name) =>
{
return new PostgresContentDbContext(name, connectionString, jsonSerializer);
builder.UseBulkInsertPostgreSql();
builder.UseNpgsql(connectionString);
})
.AddSingleton<ConnectionStringParser, PostgresConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextPostgres>>().Done()

11
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<TestDbContextSqlServer>(b =>
.AddPooledDbContextFactory<TestDbContextSqlServer>(builder =>
{
b.UseSqlServer(connectionString, options =>
builder.UseBulkInsertSqlServer();
builder.UseSqlServer(connectionString, options =>
{
options.UseNetTopologySuite();
});
})
.AddNamedDbContext((jsonSerializer, name) =>
.AddNamedDbContext<SqlServerContentDbContext>((builder, name) =>
{
return new SqlServerContentDbContext(name, connectionString, jsonSerializer);
builder.UseBulkInsertSqlServer();
builder.UseSqlServer(connectionString);
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextSqlServer>>().Done()

21
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)

6
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;

2
frontend/src/app/features/rules/pages/rule/step-dialog.component.html

@ -18,6 +18,7 @@
</div>
</div>
<div class="form-group row">
<div class="col-9 offset-3">
<div class="form-check">
<input
@ -30,6 +31,7 @@
</div>
<sqx-form-hint> {{ "rules.stepIgnoreErrorHint" | sqxTranslate }} </sqx-form-hint>
</div>
</div>
<hr />

2
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,
),
});

Loading…
Cancel
Save