Browse Source

fix: move JSON function creation into EF Core migrations to prevent startup failure on restart (#1313)

* fix: use CREATE OR ALTER/REPLACE FUNCTION to prevent startup failure on restart

On application restart the hosted service fails with:
  SqlException: There is already an object named 'json_exists' in the database.

The #if RELEASE guard in JsonFunction.cs skips DROP FUNCTION IF EXISTS
statements in release builds, so CREATE FUNCTION fails if the functions
already exist from a previous run.

Replace the DROP + CREATE pattern with idempotent alternatives:
- SQL Server: CREATE OR ALTER FUNCTION (supported since SQL Server 2016 SP1)
- MySQL:      CREATE OR REPLACE FUNCTION

PostgreSQL was already using CREATE OR REPLACE FUNCTION correctly.
DROP FUNCTION IF EXISTS statements are removed from both SQL files.

* Move JSON function creation into EF Core migrations

- Replace SqlDialectInitializer startup logic with proper EF Core migrations
  for all three providers (MySQL, SQL Server, Postgres)
- SQL Server: uses CREATE OR ALTER FUNCTION (idempotent, no DROP needed)
- MySQL: uses DROP FUNCTION IF EXISTS + CREATE FUNCTION in migration
- Remove SqlDialectInitializer registration from production ServiceExtensions
- Add migration tests: idempotency and upgrade-from-pre-migration-database

* Add Postgres migration tests; fix image tag for arm64 compatibility

* Replace DatabaseCreator+SqlDialectInitializer with DatabaseMigrator in test fixtures

- Test fixtures now use the same code path as production (MigrateAsync)
- DatabaseCreator and SqlDialectInitializer are no longer needed and deleted
- Functions are created via the AddJsonFunctions migration, not at every startup

* Fix missing Squidex.Infrastructure using in test fixtures

* Fix test fixtures: use EnsureCreated+Dialect.InitializeAsync and per-prefix migration history

Two bugs fixed in the EF Core test fixtures (PostgresFixture, MySqlFixture, SqlServerFixture):

1. Replace DatabaseMigrator with EnsureCreatedAsync + Dialect.InitializeAsync

   TestDbContext* are test-only contexts with no EF migration files, so
   DatabaseMigrator<TestDbContext*>.InitializeAsync called MigrateAsync which was
   a complete no-op — no tables were ever created and all integration tests failed
   with 'relation does not exist'.

   EnsureCreatedAsync builds the schema directly from the EF Core model, which is
   the correct approach for contexts without migrations. Dialect.InitializeAsync is
   then called explicitly to create the database-specific JSON functions (json_exists
   etc.) that EnsureCreated does not set up.

2. Add per-prefix MigrationsHistoryTable for named ContentDbContext registrations

   DynamicTables.PrepareAsync calls MigrateAsync on the named ContentDbContext
   (e.g. PostgresContentDbContext) to create per-app/schema dedicated tables such
   as '__c5_ContentsAll'. The migration (AddInitial) reads TableName.Prefix to
   build the table name at runtime.

   All named contexts shared the default '__EFMigrationsHistory' table, so after
   the first prefix ran AddInitial and recorded it, every subsequent prefix saw the
   migration as already applied and skipped it — leaving its dedicated tables
   uncreated and causing 'relation __cN_ContentsAll does not exist' failures in
   all but the first dedicated-table test.

   Setting options.MigrationsHistoryTable(\$"{name}MigrationHistory") gives each
   prefix its own independent migration history, so AddInitial runs once per prefix
   and creates the correct tables each time.

Also add *.lscache to .gitignore (C# language server cache files).
master
Joe 2 days ago
committed by GitHub
parent
commit
8a4cae16dd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .gitignore
  2. 42
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs
  3. 26
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialectInitializer.cs
  4. 1628
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.Designer.cs
  5. 67
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.cs
  6. 7
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/JsonFunction.cs
  7. 1629
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.Designer.cs
  8. 59
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.cs
  9. 1631
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.Designer.cs
  10. 64
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.cs
  11. 66
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/json_function.sql
  12. 1
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  13. 87
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs
  14. 79
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs
  15. 77
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs
  16. 11
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs
  17. 17
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs
  18. 15
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs

3
.gitignore

@ -35,3 +35,6 @@ launchSettings.json
/frontend/app-config/localhost-key.pem
/frontend/app-config/localhost.pem
# C# language server cache
*.lscache

42
backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseCreator.cs

@ -1,42 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Squidex.Hosting;
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Infrastructure.Migrations;
public sealed class DatabaseCreator<TContext>(IDbContextFactory<TContext> dbContextFactory) : IInitializable
where TContext : DbContext
{
private static readonly TimeSpan WaitTime = TimeSpan.FromSeconds(30);
public int Order => -1000;
public async Task InitializeAsync(
CancellationToken ct)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);
using var cts = new CancellationTokenSource(WaitTime);
while (!await dbContext.Database.CanConnectAsync(cts.Token))
{
await Task.Delay(100, cts.Token);
}
if (dbContext.Database.GetService<IDatabaseCreator>() is not RelationalDatabaseCreator relationalDatabaseCreator)
{
return;
}
await relationalDatabaseCreator.EnsureCreatedAsync(ct);
}
}

26
backend/src/Squidex.Data.EntityFramework/Infrastructure/Queries/SqlDialectInitializer.cs

@ -1,26 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.EntityFrameworkCore;
using Squidex.Hosting;
namespace Squidex.Infrastructure.Queries;
public sealed class SqlDialectInitializer<TContext>(IDbContextFactory<TContext> dbContextFactory)
: IInitializable where TContext : DbContext
{
public async Task InitializeAsync(CancellationToken ct)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);
if (dbContext is not IDbContextWithDialect withDialect)
{
return;
}
await withDialect.Dialect.InitializeAsync(dbContext, ct);
}
}

1628
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

67
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20260512000000_AddJsonFunctions.cs

@ -0,0 +1,67 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.MySql;
#nullable disable
namespace Squidex.Providers.MySql.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(MySqlDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.MySql.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement, suppressTransaction: true);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
var functions = new[]
{
"json_empty",
"json_exists",
"json_null_equals",
"json_null_notequals",
"json_text_contains",
"json_text_endswith",
"json_text_equals",
"json_text_greaterthan",
"json_text_greaterthanorequal",
"json_text_in",
"json_text_lessthan",
"json_text_lessthanorequal",
"json_text_matchs",
"json_text_notequals",
"json_text_startswith",
"json_boolean_equals",
"json_boolean_in",
"json_boolean_notequals",
"json_number_equals",
"json_number_greaterthan",
"json_number_greaterthanorequal",
"json_number_in",
"json_number_lessthan",
"json_number_lessthanorequal",
"json_number_notequals",
};
foreach (var function in functions)
{
migrationBuilder.Sql($"DROP FUNCTION IF EXISTS {function}", suppressTransaction: true);
}
}
}
}

7
backend/src/Squidex.Data.EntityFramework/Providers/MySql/JsonFunction.cs

@ -57,15 +57,8 @@ public static class JsonFunction
sqlText = sqlText.Replace("}", "}}", StringComparison.Ordinal);
var statements = sqlText.Split(";;", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// We want to filter out the drop statements and multiple function creations are not supported.
foreach (var statement in statements)
{
#if RELEASE
if (statement.StartsWith("DROP", StringComparison.Ordinal))
{
continue;
}
#endif
await dbContext.Database.ExecuteSqlRawAsync(statement, ct);
}
}

1629
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

59
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20260512000002_AddJsonFunctions.cs

@ -0,0 +1,59 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.Postgres;
#nullable disable
namespace Squidex.Providers.Postgres.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(PostgresDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.Postgres.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_empty(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_exists(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_null_equals(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_null_notequals(jsonb)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_equals(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_notequals(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_lessthan(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_lessthanorequal(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_greaterthan(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_greaterthanorequal(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_contains(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_startswith(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_endswith(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_matchs(jsonb, text)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_text_in(jsonb, text[])");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_equals(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_notequals(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_lessthan(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_lessthanorequal(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_greaterthan(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_greaterthanorequal(jsonb, numeric)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_number_in(jsonb, numeric[])");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_equals(jsonb, boolean)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_notequals(jsonb, boolean)");
migrationBuilder.Sql("DROP FUNCTION IF EXISTS jsonb_boolean_in(jsonb, boolean[])");
}
}
}

1631
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.Designer.cs

File diff suppressed because it is too large

64
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20260512000001_AddJsonFunctions.cs

@ -0,0 +1,64 @@
using System.IO;
using Microsoft.EntityFrameworkCore.Migrations;
using Squidex.Providers.SqlServer;
#nullable disable
namespace Squidex.Providers.SqlServer.App.Migrations
{
/// <inheritdoc />
public partial class AddJsonFunctions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
var assembly = typeof(SqlServerDialect).Assembly;
using var sqlStream = assembly.GetManifestResourceStream("Squidex.Providers.SqlServer.json_function.sql")!;
using var reader = new StreamReader(sqlStream);
var sqlText = reader.ReadToEnd();
var statements = sqlText.Split(";;", System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var statement in statements)
{
migrationBuilder.Sql(statement);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
var functions = new[]
{
"dbo.json_empty",
"dbo.json_exists",
"dbo.json_null_equals",
"dbo.json_null_notequals",
"dbo.json_text_contains",
"dbo.json_text_endswith",
"dbo.json_text_equals",
"dbo.json_text_greaterthan",
"dbo.json_text_greaterthanorequal",
"dbo.json_text_lessthan",
"dbo.json_text_lessthanorequal",
"dbo.json_text_matchs",
"dbo.json_text_notequals",
"dbo.json_text_startswith",
"dbo.json_boolean_equals",
"dbo.json_boolean_notequals",
"dbo.json_number_equals",
"dbo.json_number_greaterthan",
"dbo.json_number_greaterthanorequal",
"dbo.json_number_lessthan",
"dbo.json_number_lessthanorequal",
"dbo.json_number_notequals",
};
foreach (var function in functions)
{
migrationBuilder.Sql($"DROP FUNCTION IF EXISTS {function}");
}
}
}
}

66
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/json_function.sql

@ -1,8 +1,7 @@
-- =============================================================================
-- TYPE-AGNOSTIC
-- =============================================================================
DROP FUNCTION IF EXISTS dbo.json_empty;;
CREATE FUNCTION dbo.json_empty(@col NVARCHAR(MAX), @path NVARCHAR(500))
CREATE OR ALTER FUNCTION dbo.json_empty(@col NVARCHAR(MAX), @path NVARCHAR(500))
RETURNS BIT
AS
BEGIN
@ -17,8 +16,7 @@ BEGIN
RETURN 0;
END;;
DROP FUNCTION IF EXISTS dbo.json_exists;;
CREATE FUNCTION dbo.json_exists(@col NVARCHAR(MAX), @path NVARCHAR(500))
CREATE OR ALTER FUNCTION dbo.json_exists(@col NVARCHAR(MAX), @path NVARCHAR(500))
RETURNS BIT
AS
BEGIN
@ -29,8 +27,7 @@ END;;
-- =============================================================================
-- NULL
-- =============================================================================
DROP FUNCTION IF EXISTS dbo.json_null_equals;;
CREATE FUNCTION dbo.json_null_equals(@col NVARCHAR(MAX), @path NVARCHAR(500))
CREATE OR ALTER FUNCTION dbo.json_null_equals(@col NVARCHAR(MAX), @path NVARCHAR(500))
RETURNS BIT
AS
BEGIN
@ -42,8 +39,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) IS NULL THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_null_notequals;;
CREATE FUNCTION dbo.json_null_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500))
CREATE OR ALTER FUNCTION dbo.json_null_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500))
RETURNS BIT
AS
BEGIN
@ -54,8 +50,7 @@ END;;
-- =============================================================================
-- TEXT
-- =============================================================================
DROP FUNCTION IF EXISTS dbo.json_text_equals;;
CREATE FUNCTION dbo.json_text_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -67,8 +62,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) = @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_notequals;;
CREATE FUNCTION dbo.json_text_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -80,8 +74,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) != @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_lessthan;;
CREATE FUNCTION dbo.json_text_lessthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_lessthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -93,8 +86,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) < @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_lessthanorequal;;
CREATE FUNCTION dbo.json_text_lessthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_lessthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -106,8 +98,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) <= @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_greaterthan;;
CREATE FUNCTION dbo.json_text_greaterthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_greaterthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -119,8 +110,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) > @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_greaterthanorequal;;
CREATE FUNCTION dbo.json_text_greaterthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_greaterthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -132,8 +122,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) >= @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_contains;;
CREATE FUNCTION dbo.json_text_contains(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_contains(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -145,8 +134,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) LIKE '%' + @target + '%' THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_startswith;;
CREATE FUNCTION dbo.json_text_startswith(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_startswith(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -158,8 +146,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) LIKE @target + '%' THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_endswith;;
CREATE FUNCTION dbo.json_text_endswith(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_endswith(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -171,8 +158,7 @@ BEGIN
RETURN CASE WHEN JSON_VALUE(@col, @path) LIKE '%' + @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_text_matchs;;
CREATE FUNCTION dbo.json_text_matchs(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
CREATE OR ALTER FUNCTION dbo.json_text_matchs(@col NVARCHAR(MAX), @path NVARCHAR(500), @target NVARCHAR(2000))
RETURNS BIT
AS
BEGIN
@ -188,8 +174,7 @@ END;;
-- =============================================================================
-- NUMBER
-- =============================================================================
DROP FUNCTION IF EXISTS dbo.json_number_equals;;
CREATE FUNCTION dbo.json_number_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -203,8 +188,7 @@ BEGIN
RETURN CASE WHEN TRY_CAST(JSON_VALUE(@col, @path) AS DECIMAL(38, 10)) = @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_number_notequals;;
CREATE FUNCTION dbo.json_number_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -218,8 +202,7 @@ BEGIN
RETURN CASE WHEN TRY_CAST(JSON_VALUE(@col, @path) AS DECIMAL(38, 10)) != @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_number_lessthan;;
CREATE FUNCTION dbo.json_number_lessthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_lessthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -233,8 +216,7 @@ BEGIN
RETURN CASE WHEN TRY_CAST(JSON_VALUE(@col, @path) AS DECIMAL(38, 10)) < @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_number_lessthanorequal;;
CREATE FUNCTION dbo.json_number_lessthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_lessthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -248,8 +230,7 @@ BEGIN
RETURN CASE WHEN TRY_CAST(JSON_VALUE(@col, @path) AS DECIMAL(38, 10)) <= @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_number_greaterthan;;
CREATE FUNCTION dbo.json_number_greaterthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_greaterthan(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -263,8 +244,7 @@ BEGIN
RETURN CASE WHEN TRY_CAST(JSON_VALUE(@col, @path) AS DECIMAL(38, 10)) > @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_number_greaterthanorequal;;
CREATE FUNCTION dbo.json_number_greaterthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
CREATE OR ALTER FUNCTION dbo.json_number_greaterthanorequal(@col NVARCHAR(MAX), @path NVARCHAR(500), @target DECIMAL(38, 10))
RETURNS BIT
AS
BEGIN
@ -282,8 +262,7 @@ END;;
-- =============================================================================
-- BOOLEAN
-- =============================================================================
DROP FUNCTION IF EXISTS dbo.json_boolean_equals;;
CREATE FUNCTION dbo.json_boolean_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target BIT)
CREATE OR ALTER FUNCTION dbo.json_boolean_equals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target BIT)
RETURNS BIT
AS
BEGIN
@ -297,8 +276,7 @@ BEGIN
RETURN CASE WHEN IIF(JSON_VALUE(@col, @path) = 'true', 1, IIF(JSON_VALUE(@col, @path) = 'false', 0, NULL)) = @target THEN 1 ELSE 0 END;
END;;
DROP FUNCTION IF EXISTS dbo.json_boolean_notequals;;
CREATE FUNCTION dbo.json_boolean_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target BIT)
CREATE OR ALTER FUNCTION dbo.json_boolean_notequals(@col NVARCHAR(MAX), @path NVARCHAR(500), @target BIT)
RETURNS BIT
AS
BEGIN

1
backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs

@ -271,7 +271,6 @@ public static class ServiceExtensions
.AddEntityFrameworkStore<TContext, CronJobContext>();
services.AddEntityFrameworkAssetKeyValueStore<TContext, TusMetadata>();
services.AddSingletonAs<SqlDialectInitializer<TContext>>();
}
public static void AddSquidexEntityFrameworkEventStore(this IServiceCollection services, IConfiguration config)

87
backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs

@ -8,7 +8,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
using Testcontainers.MySql;
@ -18,7 +20,9 @@ namespace Squidex.EntityFramework.Migrations;
[Trait("Category", "TestContainer")]
public class MySqlMigrationTests : IAsyncLifetime
{
private readonly MySqlContainer mysql = new MySqlBuilder("mysql:8.0").Build();
private readonly MySqlContainer mysql = new MySqlBuilder("mysql:8.0")
.WithCommand("--log-bin-trust-function-creators=1")
.Build();
public async ValueTask InitializeAsync()
{
@ -60,4 +64,85 @@ public class MySqlMigrationTests : IAsyncLifetime
var migrations = await dbContext.Database.GetAppliedMigrationsAsync();
Assert.NotEmpty(migrations);
}
[Fact]
public async Task Should_migrate_idempotent_and_functions_callable()
{
var connectionString = mysql.GetConnectionString();
var services =
new ServiceCollection()
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<MySqlAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<MySqlAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<MySqlAppDbContext>>();
// Run migrations twice to verify idempotency.
await databaseMigrator.InitializeAsync(default);
await databaseMigrator.InitializeAsync(default);
// Verify the json_exists function was created and is callable.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var dbContext = await databaseFactory.CreateDbContextAsync();
var result = await dbContext.Database.ExecuteSqlRawAsync(
"SELECT json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
[Fact]
public async Task Should_migrate_when_functions_already_exist()
{
var connectionString = mysql.GetConnectionString();
var services =
new ServiceCollection()
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<MySqlAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<MySqlAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<MySqlAppDbContext>>();
// Simulate a pre-migration database: apply all migrations up to (but not including)
// AddJsonFunctions, then create the functions via the old SqlDialectInitializer path.
await using (var dbContext = await databaseFactory.CreateDbContextAsync())
{
await dbContext.Database.MigrateAsync("20260323154443_MigrateToNet10");
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
// Run the full migration — should apply AddJsonFunctions on top of already-existing functions.
await databaseMigrator.InitializeAsync(default);
// Verify the functions are still callable after migration.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var verifyContext = await databaseFactory.CreateDbContextAsync();
var result = await verifyContext.Database.ExecuteSqlRawAsync(
"SELECT json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
}

79
backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs

@ -8,7 +8,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
using Testcontainers.PostgreSql;
@ -19,7 +21,7 @@ namespace Squidex.EntityFramework.Migrations;
public class PostgresMigrationTests : IAsyncLifetime
{
private readonly PostgreSqlContainer postgreSql =
new PostgreSqlBuilder("postgis/postgis")
new PostgreSqlBuilder("imresamu/postgis:16-3.4")
.Build();
public async ValueTask InitializeAsync()
@ -59,4 +61,79 @@ public class PostgresMigrationTests : IAsyncLifetime
var migrations = await dbContext.Database.GetAppliedMigrationsAsync();
Assert.NotEmpty(migrations);
}
[Fact]
public async Task Should_migrate_idempotent_and_functions_callable()
{
var services =
new ServiceCollection()
.AddDbContextFactory<PostgresAppDbContext>(b =>
{
b.UseNpgsql(postgreSql.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, PostgresConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<PostgresAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<PostgresAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<PostgresAppDbContext>>();
// Run migrations twice to verify idempotency.
await databaseMigrator.InitializeAsync(default);
await databaseMigrator.InitializeAsync(default);
// Verify the jsonb_exists function was created and is callable.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var dbContext = await databaseFactory.CreateDbContextAsync();
var result = await dbContext.Database.ExecuteSqlRawAsync(
"SELECT jsonb_exists('{{\"a\":1}}'::jsonb)");
Assert.Equal(-1, result);
}
[Fact]
public async Task Should_migrate_when_functions_already_exist()
{
var services =
new ServiceCollection()
.AddDbContextFactory<PostgresAppDbContext>(b =>
{
b.UseNpgsql(postgreSql.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, PostgresConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<PostgresAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<PostgresAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<PostgresAppDbContext>>();
// Simulate a pre-migration database: apply all migrations up to (but not including)
// AddJsonFunctions, then create the functions via the old SqlDialectInitializer path.
await using (var dbContext = await databaseFactory.CreateDbContextAsync())
{
await dbContext.Database.MigrateAsync("20260323155026_MigrateToNet10");
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
// Run the full migration — should apply AddJsonFunctions on top of already-existing functions.
await databaseMigrator.InitializeAsync(default);
// Verify the functions are still callable after migration.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var verifyContext = await databaseFactory.CreateDbContextAsync();
var result = await verifyContext.Database.ExecuteSqlRawAsync(
"SELECT jsonb_exists('{{\"a\":1}}'::jsonb)");
Assert.Equal(-1, result);
}
}

77
backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs

@ -8,7 +8,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
using Testcontainers.MsSql;
@ -59,4 +61,79 @@ public class SqlServerMigrationTests : IAsyncLifetime
var migrations = await dbContext.Database.GetAppliedMigrationsAsync();
Assert.NotEmpty(migrations);
}
[Fact]
public async Task Should_migrate_idempotent_and_functions_callable()
{
var services =
new ServiceCollection()
.AddDbContextFactory<SqlServerAppDbContext>(b =>
{
b.UseSqlServer(sqlServer.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<SqlServerAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<SqlServerAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<SqlServerAppDbContext>>();
// Run migrations twice to verify idempotency.
await databaseMigrator.InitializeAsync(default);
await databaseMigrator.InitializeAsync(default);
// Verify the dbo.json_exists function was created and is callable.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var dbContext = await databaseFactory.CreateDbContextAsync();
var result = await dbContext.Database.ExecuteSqlRawAsync(
"SELECT dbo.json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
[Fact]
public async Task Should_migrate_when_functions_already_exist()
{
var services =
new ServiceCollection()
.AddDbContextFactory<SqlServerAppDbContext>(b =>
{
b.UseSqlServer(sqlServer.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<SqlServerAppDbContext>>()
.BuildServiceProvider();
var databaseMigrator = services.GetRequiredService<DatabaseMigrator<SqlServerAppDbContext>>();
var databaseFactory = services.GetRequiredService<IDbContextFactory<SqlServerAppDbContext>>();
// Simulate a pre-migration database: apply all migrations up to (but not including)
// AddJsonFunctions, then create the functions via the old SqlDialectInitializer path.
await using (var dbContext = await databaseFactory.CreateDbContextAsync())
{
await dbContext.Database.MigrateAsync("20260323155035_MigrateToNet10");
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
// Run the full migration — should apply AddJsonFunctions on top of already-existing functions.
await databaseMigrator.InitializeAsync(default);
// Verify the functions are still callable after migration.
// Note: {{ and }} are escaped braces for ExecuteSqlRawAsync's string.Format-style parser.
await using var verifyContext = await databaseFactory.CreateDbContextAsync();
var result = await verifyContext.Database.ExecuteSqlRawAsync(
"SELECT dbo.json_exists('{{\"a\":1}}', '$.a')");
Assert.Equal(-1, result);
}
}

11
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs

@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.Content;
using Testcontainers.MySql;
@ -60,21 +59,27 @@ public class MySqlFixture(string? reuseId = null) : IAsyncLifetime, ISqlContentF
builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
options.MigrationsHistoryTable($"{name}MigrationHistory");
});
builder.ConfigureWarnings(w =>
w.Ignore(RelationalEventId.PendingModelChangesWarning));
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextMySql>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<IInitializable, SqlDialectInitializer<TestDbContextMySql>>()
.BuildServiceProvider();
foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.InitializeAsync(default);
}
await using var dbContext = await services.GetRequiredService<IDbContextFactory<TestDbContextMySql>>().CreateDbContextAsync();
await dbContext.Database.EnsureCreatedAsync();
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
public async ValueTask DisposeAsync()

17
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs

@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.Content;
using Testcontainers.PostgreSql;
@ -23,7 +22,7 @@ namespace Squidex.EntityFramework.TestHelpers;
public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixture<TestDbContextPostgres, PostgresContentDbContext>
{
private readonly PostgreSqlContainer postgreSql =
new PostgreSqlBuilder("postgis/postgis")
new PostgreSqlBuilder("imresamu/postgis:16-3.4")
.WithReuse(true)
.WithLabel("reuse-id", reuseId)
.Build();
@ -55,21 +54,29 @@ public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixtu
.AddNamedDbContext<PostgresContentDbContext>((builder, name) =>
{
builder.UseBulkInsertPostgreSql();
builder.UseNpgsql(connectionString);
builder.UseNpgsql(connectionString, options =>
{
options.MigrationsHistoryTable($"{name}MigrationHistory");
});
builder.ConfigureWarnings(w =>
w.Ignore(RelationalEventId.PendingModelChangesWarning));
})
.AddSingleton<ConnectionStringParser, PostgresConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextPostgres>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<IInitializable, SqlDialectInitializer<TestDbContextPostgres>>()
.BuildServiceProvider();
foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.InitializeAsync(default);
}
await using var dbContext = await services.GetRequiredService<IDbContextFactory<TestDbContextPostgres>>().CreateDbContextAsync();
await dbContext.Database.EnsureCreatedAsync();
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
public async ValueTask DisposeAsync()

15
backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs

@ -14,7 +14,6 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Queries;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.Content;
using Testcontainers.MsSql;
@ -57,21 +56,29 @@ public class SqlServerFixture(string? reuseId = null) : IAsyncLifetime, ISqlCont
.AddNamedDbContext<SqlServerContentDbContext>((builder, name) =>
{
builder.UseBulkInsertSqlServer();
builder.UseSqlServer(connectionString);
builder.UseSqlServer(connectionString, options =>
{
options.MigrationsHistoryTable($"{name}MigrationHistory");
});
builder.ConfigureWarnings(w =>
w.Ignore(RelationalEventId.PendingModelChangesWarning));
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextSqlServer>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<IInitializable, SqlDialectInitializer<TestDbContextSqlServer>>()
.BuildServiceProvider();
foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.InitializeAsync(default);
}
await using var dbContext = await services.GetRequiredService<IDbContextFactory<TestDbContextSqlServer>>().CreateDbContextAsync();
await dbContext.Database.EnsureCreatedAsync();
if (dbContext is IDbContextWithDialect withDialect)
{
await withDialect.Dialect.InitializeAsync(dbContext, default);
}
}
public async ValueTask DisposeAsync()

Loading…
Cancel
Save