Browse Source

Batch processing for events. (#1222)

* Batch processing for events.

* Improve error handling.

* Fix tests

* More test fixes
pull/1224/head
Sebastian Stehle 1 year ago
committed by GitHub
parent
commit
29d7dd3cfd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      backend/.editorconfig
  2. 52
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs
  3. 26
      backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs
  4. 1584
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250517203912_MakeAssetPropertiesNullable.Designer.cs
  5. 74
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250517203912_MakeAssetPropertiesNullable.cs
  6. 2
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs
  7. 21
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlConnectionStringParser.cs
  8. 19
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDiagnostics.cs
  9. 1585
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250517203918_MakeAssetPropertiesNullable.Designer.cs
  10. 54
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250517203918_MakeAssetPropertiesNullable.cs
  11. 2
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs
  12. 21
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresConnectionStringParser.cs
  13. 20
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDiagnostics.cs
  14. 1587
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250517203925_MakeAssetPropertiesNullable.Designer.cs
  15. 54
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250517203925_MakeAssetPropertiesNullable.cs
  16. 2
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs
  17. 21
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerConnectionStringParser.cs
  18. 18
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDiagnostics.cs
  19. 21
      backend/src/Squidex.Data.EntityFramework/ServiceExtensions.cs
  20. 15
      backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj
  21. 12
      backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj
  22. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/Asset.cs
  23. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  24. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  25. 35
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs
  26. 1
      backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs
  27. 1
      backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs
  28. 1
      backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs
  29. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  30. 20
      backend/src/Squidex.Infrastructure/States/Persistence.cs
  31. 1
      backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs
  32. 38
      backend/src/Squidex/Config/Startup/LogConfigurationHost.cs
  33. 24
      backend/src/Squidex/Squidex.csproj
  34. 147
      backend/tests/RunCoverage.ps1
  35. 23
      backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/MySqlConnectionStringParserTests.cs
  36. 23
      backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/PostgresConnectionStringParserTests.cs
  37. 23
      backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/SqlServerConnectionStringParserTests.cs
  38. 28
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/MySqlMigrationTests.cs
  39. 22
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/PostgresMigrationTests.cs
  40. 22
      backend/tests/Squidex.Data.Tests/EntityFramework/Migrations/SqlServerMigrationTests.cs
  41. 2
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/MySqlFixture.cs
  42. 2
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/PostgresFixture.cs
  43. 2
      backend/tests/Squidex.Data.Tests/EntityFramework/TestHelpers/SqlServerFixture.cs
  44. 9
      backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj
  45. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  46. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs
  47. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  48. 4
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  49. 4
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  50. 4
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  51. 16
      backend/tests/coverlet.runsettings.xml
  52. 60
      backend/tests/tests.sln

3
backend/.editorconfig

@ -180,5 +180,8 @@ dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none
# SYSLIB1045: Convert to 'GeneratedRegexAttribute'.
dotnet_diagnostic.SYSLIB1045.severity = none
# xUnit1033: Test classes decorated with 'Xunit.IClassFixture<TFixture>' or 'Xunit.ICollectionFixture<TFixture>' should add a constructor argument of type TFixture
dotnet_diagnostic.xUnit1033.severity = none

52
backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/ConnectionStringParser.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Data.Common;
namespace Squidex.Infrastructure.Migrations;
public class ConnectionStringParser
{
public string? GetHostName(string? source)
{
if (string.IsNullOrEmpty(source))
{
return null;
}
try
{
return GetProviderSpecificHostName(source);
}
catch
{
try
{
var builder = new DbConnectionStringBuilder
{
ConnectionString = source
};
if (builder.TryGetValue("Server", out var server))
{
return server?.ToString();
}
return null;
}
catch
{
return null;
}
}
}
protected virtual string? GetProviderSpecificHostName(string source)
{
throw new NotFiniteNumberException();
}
}

26
backend/src/Squidex.Data.EntityFramework/Infrastructure/Migrations/DatabaseMigrator.cs

@ -12,7 +12,7 @@ using Squidex.Hosting;
namespace Squidex.Infrastructure.Migrations;
public sealed class DatabaseMigrator<TContext>(IDbContextFactory<TContext> dbContextFactory) : IInitializable
public sealed class DatabaseMigrator<TContext>(IDbContextFactory<TContext> dbContextFactory, ConnectionStringParser parser) : IInitializable
where TContext : DbContext
{
private static readonly TimeSpan WaitTime = TimeSpan.FromSeconds(30);
@ -24,10 +24,28 @@ public sealed class DatabaseMigrator<TContext>(IDbContextFactory<TContext> dbCon
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);
using var cts = new CancellationTokenSource(WaitTime);
while (!await dbContext.Database.CanConnectAsync(cts.Token))
try
{
await Task.Delay(100, cts.Token);
using var cts = new CancellationTokenSource(WaitTime);
while (!await dbContext.Database.CanConnectAsync(cts.Token))
{
await Task.Delay(100, cts.Token);
}
cts.Token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
var connectionString = dbContext.Database.GetConnectionString();
var hostName = parser.GetHostName(connectionString);
if (string.IsNullOrWhiteSpace(hostName))
{
hostName = "Unknown";
}
throw new InvalidOperationException($"Failed to connect to database <{hostName}>");
}
await dbContext.Database.MigrateAsync(ct);

1584
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250517203912_MakeAssetPropertiesNullable.Designer.cs

File diff suppressed because it is too large

74
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/20250517203912_MakeAssetPropertiesNullable.cs

@ -0,0 +1,74 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.MySql.App.Migrations
{
/// <inheritdoc />
public partial class MakeAssetPropertiesNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "Assets",
keyColumn: "Slug",
keyValue: null,
column: "Slug",
value: "");
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "Assets",
keyColumn: "FileHash",
keyValue: null,
column: "FileHash",
value: "");
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

2
backend/src/Squidex.Data.EntityFramework/Providers/MySql/App/Migrations/MySqlDbContextModelSnapshot.cs

@ -426,7 +426,6 @@ namespace Squidex.Providers.MySql.Migrations
.HasColumnType("varchar(100)");
b.Property<string>("FileHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("FileName")
@ -477,7 +476,6 @@ namespace Squidex.Providers.MySql.Migrations
.HasColumnType("varchar(255)");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")

21
backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlConnectionStringParser.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MySqlConnector;
using Squidex.Infrastructure.Migrations;
namespace Squidex.Providers.MySql;
public sealed class MySqlConnectionStringParser : ConnectionStringParser
{
protected override string? GetProviderSpecificHostName(string source)
{
var builder = new MySqlConnectionStringBuilder(source);
return builder.Server;
}
}

19
backend/src/Squidex.Data.EntityFramework/Providers/MySql/MySqlDiagnostics.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using OpenTelemetry.Trace;
using Squidex.Infrastructure;
namespace Squidex.Providers.MySql;
public sealed class MySqlDiagnostics : ITelemetryConfigurator
{
public void Configure(TracerProviderBuilder builder)
{
builder.AddSource("MySqlConnector");
}
}

1585
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250517203918_MakeAssetPropertiesNullable.Designer.cs

File diff suppressed because it is too large

54
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/20250517203918_MakeAssetPropertiesNullable.cs

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.Postgres.App.Migrations
{
/// <inheritdoc />
public partial class MakeAssetPropertiesNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
}
}

2
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Migrations/PostgresDbContextModelSnapshot.cs

@ -427,7 +427,6 @@ namespace Squidex.Providers.Postgres.Migrations
.HasColumnType("character varying(100)");
b.Property<string>("FileHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FileName")
@ -478,7 +477,6 @@ namespace Squidex.Providers.Postgres.Migrations
.HasColumnType("character varying(255)");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Tags")

21
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresConnectionStringParser.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Npgsql;
using Squidex.Infrastructure.Migrations;
namespace Squidex.Providers.Postgres;
public class PostgresConnectionStringParser : ConnectionStringParser
{
protected override string? GetProviderSpecificHostName(string source)
{
var builder = new NpgsqlConnectionStringBuilder(source);
return builder.Host;
}
}

20
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/PostgresDiagnostics.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Npgsql;
using OpenTelemetry.Trace;
using Squidex.Infrastructure;
namespace Squidex.Providers.Postgres;
public sealed class PostgresDiagnostics : ITelemetryConfigurator
{
public void Configure(TracerProviderBuilder builder)
{
builder.AddNpgsql();
}
}

1587
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250517203925_MakeAssetPropertiesNullable.Designer.cs

File diff suppressed because it is too large

54
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/20250517203925_MakeAssetPropertiesNullable.cs

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Squidex.Providers.SqlServer.App.Migrations
{
/// <inheritdoc />
public partial class MakeAssetPropertiesNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Slug",
table: "Assets",
type: "nvarchar(max)",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "FileHash",
table: "Assets",
type: "nvarchar(max)",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
}
}
}

2
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/App/Migrations/SqlServerDbContextModelSnapshot.cs

@ -429,7 +429,6 @@ namespace Squidex.Providers.SqlServer.Migrations
.HasColumnType("nvarchar(100)");
b.Property<string>("FileHash")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("FileName")
@ -480,7 +479,6 @@ namespace Squidex.Providers.SqlServer.Migrations
.HasColumnType("nvarchar(255)");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Tags")

21
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerConnectionStringParser.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Data.SqlClient;
using Squidex.Infrastructure.Migrations;
namespace Squidex.Providers.SqlServer;
public sealed class SqlServerConnectionStringParser : ConnectionStringParser
{
protected override string? GetProviderSpecificHostName(string source)
{
var builder = new SqlConnectionStringBuilder(source);
return builder.DataSource;
}
}

18
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/SqlServerDiagnostics.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using OpenTelemetry.Trace;
using Squidex.Infrastructure;
namespace Squidex.Providers.SqlServer;
public sealed class SqlServerDiagnostics : ITelemetryConfigurator
{
public void Configure(TracerProviderBuilder builder)
{
}
}

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

@ -43,10 +43,13 @@ using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Messaging;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
using Squidex.Providers.MySql.Content;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
using Squidex.Providers.Postgres.Content;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
using Squidex.Providers.SqlServer.Content;
using YDotNet.Server.EntityFramework;
@ -87,6 +90,12 @@ public static class ServiceExtensions
services.AddSingleton(typeof(ISnapshotStore<>), typeof(MySqlSnapshotStore<>));
services.AddSquidexEntityFramework<MySqlAppDbContext, MySqlContentDbContext>(config);
services.AddSingletonAs<MySqlDiagnostics>()
.As<ITelemetryConfigurator>();
services.AddSingletonAs<MySqlConnectionStringParser>()
.As<ConnectionStringParser>();
},
["Postgres"] = () =>
{
@ -106,6 +115,12 @@ public static class ServiceExtensions
services.AddSingleton(typeof(ISnapshotStore<>), typeof(PostgresSnapshotStore<>));
services.AddSquidexEntityFramework<PostgresAppDbContext, PostgresContentDbContext>(config);
services.AddSingletonAs<PostgresDiagnostics>()
.As<ITelemetryConfigurator>();
services.AddSingletonAs<PostgresConnectionStringParser>()
.As<ConnectionStringParser>();
},
["SqlServer"] = () =>
{
@ -125,6 +140,12 @@ public static class ServiceExtensions
services.AddSingleton(typeof(ISnapshotStore<>), typeof(SqlServerSnapshotStore<>));
services.AddSquidexEntityFramework<SqlServerAppDbContext, SqlServerContentDbContext>(config);
services.AddSingletonAs<SqlServerDiagnostics>()
.As<ITelemetryConfigurator>();
services.AddSingletonAs<SqlServerConnectionStringParser>()
.As<ConnectionStringParser>();
},
});
}

15
backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj

@ -32,21 +32,22 @@
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="8.0.11" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="8.0.7" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="8.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.NetTopologySuite" Version="8.0.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.15.0" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.18.0" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.Core" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.MySql" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.PostgreSQL" Version="8.1.6" />
<PackageReference Include="Squidex.EFCore.BulkExtensions.SqlServer" Version="8.1.6" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.15.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.15.0" />
<PackageReference Include="Squidex.Hosting" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.15.0" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="7.18.0" />
<PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

12
backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj

@ -25,12 +25,12 @@
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.30.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.15.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.15.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.15.0" />
<PackageReference Include="Squidex.Hosting" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.15.0" />
<PackageReference Include="Squidex.AI.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Events.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Flows.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="7.18.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.8.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

4
backend/src/Squidex.Domain.Apps.Core.Model/Assets/Asset.cs

@ -14,11 +14,11 @@ public record Asset : AssetItem
{
public string FileName { get; set; }
public string FileHash { get; set; }
public string? FileHash { get; set; }
public string MimeType { get; set; }
public string Slug { get; set; }
public string? Slug { get; set; }
public long FileSize { get; set; }

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -20,7 +20,7 @@
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Flows" Version="7.15.0" />
<PackageReference Include="Squidex.Flows" Version="7.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -29,8 +29,8 @@
<PackageReference Include="NJsonSchema" Version="11.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.15.0" />
<PackageReference Include="Squidex.AI" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

35
backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreJob.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
using Squidex.Domain.Apps.Core.Apps;
@ -106,7 +107,6 @@ public sealed class RestoreJob(
await context.LogAsync(" * Restore all objects like app, schemas and contents");
await context.LogAsync(" * Complete the restore operation for all objects");
await context.FlushAsync();
log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", context.Job.Id, state.Url);
state.Reader = await DownloadAsync(context, state, ct);
@ -145,7 +145,6 @@ public sealed class RestoreJob(
// Add the current user to the app, so that the admin can see it and verify integrity.
await AssignContributorAsync(context, state);
await context.LogAsync("Completed, Yeah!");
log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", context.Job.Id, state.Url);
@ -239,9 +238,7 @@ public sealed class RestoreJob(
using (Telemetry.Activities.StartActivity("Download"))
{
await run.LogAsync("Downloading Backup");
var reader = await backupArchiveLocation.OpenReaderAsync(state.Url, run.Job.Id, ct);
await run.LogAsync("Downloaded Backup");
return reader;
@ -264,15 +261,27 @@ public sealed class RestoreJob(
},
async (batch, ct) =>
{
var commits =
batch.Select(item =>
EventCommitBuilder.Create(
item.Stream,
item.Offset,
item.Event,
eventFormatter));
using (var activity = Telemetry.Activities.StartActivity("StoreEvents"))
{
var commits =
batch.Select(item =>
EventCommitBuilder.Create(
item.Stream,
item.Offset,
item.Event,
eventFormatter))
.ToList();
activity?.SetTag("totalCommits", commits.Count);
activity?.SetTag("totalEvents", commits.Sum(x => x.Events.Count));
if (commits.Any(x => x.StreamName.Contains("46b2fb05-3438-4b99-8c1d-bac8925a33dd")))
{
Debugger.Break();
}
await eventStore.AppendUnsafeAsync(commits, ct);
await eventStore.AppendUnsafeAsync(commits, ct);
}
// Just in case we use parallel inserts later.
Interlocked.Add(ref handled, batch.Count);
@ -359,9 +368,7 @@ public sealed class RestoreJob(
using (Telemetry.Activities.StartActivity("CreateUsers"))
{
await run.LogAsync("Creating Users");
await userMapping.RestoreAsync(state.Reader, userResolver, ct);
await run.LogAsync("Created Users");
}

1
backend/src/Squidex.Domain.Apps.Entities/Jobs/JobProcessor.cs

@ -205,6 +205,7 @@ public sealed class JobProcessor
{
try
{
using var activity = Telemetry.Activities.StartActivity($"Job {runner.Name}");
await SetStatusAsync(context, JobStatus.Started);
using (localCache.StartContext())

1
backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/Guards/GuardRule.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;

1
backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules;

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,13 +24,13 @@
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="7.15.0" />
<PackageReference Include="Squidex.Caching" Version="7.15.0" />
<PackageReference Include="Squidex.Events" Version="7.15.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.15.0" />
<PackageReference Include="Squidex.Log" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging" Version="7.15.0" />
<PackageReference Include="Squidex.Text" Version="7.15.0" />
<PackageReference Include="Squidex.Assets" Version="7.18.0" />
<PackageReference Include="Squidex.Caching" Version="7.18.0" />
<PackageReference Include="Squidex.Events" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="7.18.0" />
<PackageReference Include="Squidex.Log" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging" Version="7.18.0" />
<PackageReference Include="Squidex.Text" Version="7.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

20
backend/src/Squidex.Infrastructure/States/Persistence.cs

@ -8,8 +8,6 @@
using Squidex.Events;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement
namespace Squidex.Infrastructure.States;
internal sealed class Persistence<T>(
@ -54,8 +52,10 @@ internal sealed class Persistence<T>(
{
if (UseSnapshots)
{
using (Telemetry.Activities.StartActivity("Persistence/ReadState"))
using (var activity = Telemetry.Activities.StartActivity("Persistence/ReadState"))
{
activity?.SetTag("ownerType", ownerType.Name);
activity?.SetTag("ownerKey", ownerKey);
await snapshotStore.RemoveAsync(ownerKey, ct);
}
@ -64,8 +64,10 @@ internal sealed class Persistence<T>(
if (UseEventSourcing)
{
using (Telemetry.Activities.StartActivity("Persistence/ReadEvents"))
using (var activity = Telemetry.Activities.StartActivity("Persistence/ReadEvents"))
{
activity?.SetTag("ownerType", ownerType.Name);
activity?.SetTag("ownerKey", ownerKey);
await eventStore.DeleteAsync(StreamFilter.Name(streamName.Value), ct);
}
@ -172,8 +174,11 @@ internal sealed class Persistence<T>(
return;
}
using (Telemetry.Activities.StartActivity("Persistence/WriteState"))
using (var activity = Telemetry.Activities.StartActivity("Persistence/WriteState"))
{
activity?.SetTag("ownerType", ownerType.Name);
activity?.SetTag("ownerKey", ownerKey);
var job = new SnapshotWriteJob<T>(ownerKey, state, newVersion)
{
OldVersion = oldVersion,
@ -204,8 +209,11 @@ internal sealed class Persistence<T>(
try
{
using (Telemetry.Activities.StartActivity("Persistence/WriteEvents"))
using (var activity = Telemetry.Activities.StartActivity("Persistence/WriteEvents"))
{
activity?.SetTag("ownerType", ownerType.Name);
activity?.SetTag("ownerKey", ownerKey);
await eventStore.AppendAsync(eventCommitId, streamName.Value, oldVersion, eventData, ct);
}
}

1
backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs

@ -8,7 +8,6 @@
using GraphQL.Utilities;
using NJsonSchema;
using NJsonSchema.Generation;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;

38
backend/src/Squidex/Config/Startup/LogConfigurationHost.cs

@ -12,9 +12,11 @@ namespace Squidex.Config.Startup;
public sealed class LogConfigurationHost(IConfiguration configuration, ISemanticLog log) : IHostedService
{
private static readonly string RedactedValue = "*****";
private static readonly Regex[] SensitivePatterns =
[
// Authentication and API keys
#pragma warning disable MA0110 // Use the Regex source generator
new Regex(@"(?i)(secret|token|key|password|credential|auth|api[_-]?key)$"),
new Regex(@"(?i)^(aws|azure|google|microsoft|github)[_-]"),
new Regex(@"(?i)(jwt|bearer|oauth|saml)"),
@ -31,10 +33,9 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
// Database specific
new Regex(@"(?i)(mongodb|sqlserver|postgres|mysql)://.*"),
new Regex(@"(?i)(database|db|server|host|port|user|pass)="),
#pragma warning restore MA0110 // Use the Regex source generator
];
private static readonly string RedactedValue = "*****";
public Task StartAsync(
CancellationToken cancellationToken)
{
@ -44,16 +45,22 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
{
var logged = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var orderedConfigs = configuration.AsEnumerable()
.Where(kvp => kvp.Value != null)
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase);
var orderedConfigs =
configuration.AsEnumerable()
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase);
foreach (var (key, val) in orderedConfigs)
foreach (var (key, value) in orderedConfigs)
{
if (logged.Add(key))
if (string.IsNullOrWhiteSpace(value))
{
var keyLower = key.ToLowerInvariant();
var value = IsSensitiveKey(keyLower) || IsSensitiveValue(val) ? RedactedValue : val;
continue;
}
var keyLower = key.ToLowerInvariant();
if (logged.Add(keyLower))
{
var formattedValue = IsSensitiveKey(keyLower) || IsSensitiveValue(value) ? RedactedValue : value;
c.WriteProperty(keyLower, value);
}
@ -70,17 +77,16 @@ public sealed class LogConfigurationHost(IConfiguration configuration, ISemantic
private static bool IsSensitiveValue(string? value)
{
if (string.IsNullOrEmpty(value))
// Check for connection strings and URLs with credentials
if (string.IsNullOrEmpty(value) || !value.Contains("://", StringComparison.Ordinal))
{
return false;
}
// Check for connection strings and URLs with credentials
return value.Contains("://") && (
value.Contains("@") || // Contains username/password in URL
value.Contains(";") || // Contains connection string parameters
value.Contains("=") // Contains key-value pairs
);
// Contains username/password, connection string parameters or query strings.
return value.Contains('@', StringComparison.Ordinal)
|| value.Contains(';', StringComparison.Ordinal)
|| value.Contains('=', StringComparison.Ordinal);
}
public Task StopAsync(

24
backend/src/Squidex/Squidex.csproj

@ -59,17 +59,17 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.4.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.S3" Version="7.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="7.18.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="21.5.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.15.0" />
<PackageReference Include="Squidex.Hosting" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.15.0" />
<PackageReference Include="Squidex.Events.GetEventStore" Version="7.18.0" />
<PackageReference Include="Squidex.Hosting" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.All" Version="7.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="7.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.3" />
<PackageReference Include="YDotNet.Native" Version="0.4.3" />
@ -83,11 +83,11 @@
</ItemGroup>
<ItemGroup Condition="'$(IncludeMagick)' == 'true'">
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.15.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="7.18.0" />
</ItemGroup>
<ItemGroup Condition="'$(IncludeKafka)' == 'true'">
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.15.0" />
<PackageReference Include="Squidex.Messaging.Kafka" Version="7.18.0" />
</ItemGroup>
<PropertyGroup>

147
backend/tests/RunCoverage.ps1

@ -1,86 +1,105 @@
Param(
[switch]$infrastructure,
[switch]$appsCore,
[switch]$appsEntities,
[switch]$users,
[switch]$web,
[switch]$all
[switch]$testInfrastructure,
[switch]$testAppsCore,
[switch]$testAppsEntities,
[switch]$testUsers,
[switch]$testWeb,
[switch]$testAll,
[switch]$noClean
)
$ErrorActionPreference = "Stop"
$folderReports = ".\_test-output"
$folderHome = $env:USERPROFILE
$folderWorking = Get-Location
$versionOpenCover = "4.7.1221"
$versionReportGenerator = "5.4.1"
if (Test-Path $folderReports) {
Remove-Item $folderReports -recurse
if ($testAll) {
$testInfrastructure = $true
$testAppsCore = $true
$testAppsEntities = $true
$testUsers = $true
$testWeb = $true
}
Write-Host "Recreated '$folderReports' folder"
Write-Host "Test Infrastructure: $testInfrastructure"
Write-Host "Test Apps Core: $testAppsCore"
Write-Host "Test Apps Entities: $testAppsEntities"
Write-Host "Test Users: $testUsers"
Write-Host "Test Web: $testWeb"
New-Item -ItemType directory -Path $folderReports
if (!$noClean) {
if (Test-Path $folderReports) {
Remove-Item $folderReports -recurse
if ($all -Or $infrastructure) {
&"$folderHome\.nuget\packages\OpenCover\4.7.1221\tools\OpenCover.Console.exe" `
-register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test --filter Category!=Dependencies $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" `
-filter:"+[Squidex.*]* -[*.Tests]* -[Squidex.*]*CodeGen*" `
-excludebyattribute:*.ExcludeFromCodeCoverage* `
-skipautoprops `
-output:"$folderWorking\$folderReports\Infrastructure.xml" `
-oldStyle
Write-Host "Recreated '$folderReports' folder"
}
}
if ($all -Or $appsCore) {
&"$folderHome\.nuget\packages\OpenCover\4.7.1221\tools\OpenCover.Console.exe" `
-register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test --filter Category!=Dependencies $folderWorking\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" `
-filter:"+[Squidex.*]* -[*.Tests]* -[Squidex.*]*CodeGen*" `
-excludebyattribute:*.ExcludeFromCodeCoverage* `
-skipautoprops `
-output:"$folderWorking\$folderReports\Core.xml" `
-oldStyle
if (!(Test-Path $folderReports)) {
New-Item -ItemType directory -Path $folderReports
}
if ($all -Or $appsEntities) {
&"$folderHome\.nuget\packages\OpenCover\4.7.1221\tools\OpenCover.Console.exe" `
-register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test --filter Category!=Dependencies $folderWorking\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj" `
-filter:"+[Squidex.*]* -[*.Tests]* -[Squidex.*]*CodeGen*" `
-excludebyattribute:*.ExcludeFromCodeCoverage* `
-skipautoprops `
-output:"$folderWorking\$folderReports\Entities.xml" `
-oldStyle
if ($testInfrastructure) {
$projectName = "Squidex.Infrastructure.Tests"
dotnet test "$folderWorking\$projectName\$projectName.csproj" `
--no-restore `
--filter "Category!=Dependencies & Category!=TestContainer" `
--collect "XPlat Code Coverage" `
--results-directory "$folderReports" `
--settings "$folderWorking\coverlet.runsettings.xml"
}
if ($testAppsCore) {
$projectName = "Squidex.Domain.Apps.Core.Tests"
dotnet test "$folderWorking\$projectName\$projectName.csproj" `
--no-restore `
--filter "Category!=Dependencies & Category!=TestContainer" `
--collect "XPlat Code Coverage" `
--results-directory "$folderReports" `
--settings "$folderWorking\coverlet.runsettings.xml"
}
if ($all -Or $users) {
&"$folderHome\.nuget\packages\OpenCover\4.7.1221\tools\OpenCover.Console.exe" `
-register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test --filter Category!=Dependencies $folderWorking\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj" `
-filter:"+[Squidex.*]* -[*.Tests]* -[Squidex.*]*CodeGen*" `
-excludebyattribute:*.ExcludeFromCodeCoverage* `
-skipautoprops `
-output:"$folderWorking\$folderReports\Users.xml" `
-oldStyle
if ($testAppsEntities) {
$projectName = "Squidex.Domain.Apps.Entities.Tests"
dotnet test "$folderWorking\$projectName\$projectName.csproj" `
--no-restore `
--filter "Category!=Dependencies & Category!=TestContainer" `
--collect "XPlat Code Coverage" `
--results-directory "$folderReports" `
--settings "$folderWorking\coverlet.runsettings.xml"
}
if ($all -Or $web) {
&"$folderHome\.nuget\packages\OpenCover\4.7.1221\tools\OpenCover.Console.exe" `
-register:user `
-target:"C:\Program Files\dotnet\dotnet.exe" `
-targetargs:"test --filter Category!=Dependencies $folderWorking\Squidex.Web.Tests\Squidex.Web.Tests.csproj" `
-filter:"+[Squidex.*]* -[*.Tests]* -[Squidex.*]*CodeGen*" `
-excludebyattribute:*.ExcludeFromCodeCoverage* `
-skipautoprops `
-output:"$folderWorking\$folderReports\Web.xml" `
-oldStyle
if ($testUsers) {
$projectName = "Squidex.Domain.Users.Tests"
dotnet test "$folderWorking\$projectName\$projectName.csproj" `
--no-restore `
--filter "Category!=Dependencies & Category!=TestContainer" `
--collect "XPlat Code Coverage" `
--results-directory "$folderReports" `
--settings "$folderWorking\coverlet.runsettings.xml"
}
&"$folderHome\.nuget\packages\ReportGenerator\5.1.9\tools\net47\ReportGenerator.exe" `
-reports:"$folderWorking\$folderReports\*.xml" `
-targetdir:"$folderWorking\$folderReports\Output"
if ($testWeb) {
$projectName = "Squidex.Web.Tests"
dotnet test "$folderWorking\$projectName\$projectName.csproj" `
--no-restore `
--filter "Category!=Dependencies & Category!=TestContainer" `
--collect "XPlat Code Coverage" `
--results-directory "$folderReports" `
--settings "$folderWorking\coverlet.runsettings.xml"
}
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator `
-reports:"$folderReports\**\coverage.cobertura.xml" `
-targetdir:"$folderReports\report" `
-reporttypes:Html

23
backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/MySqlConnectionStringParserTests.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Providers.MySql;
namespace Squidex.EntityFramework.Infrastructure.Migrations;
public class MySqlConnectionStringParserTests
{
[Fact]
public void Should_parse_host_name()
{
var sut = new MySqlConnectionStringParser();
var result = sut.GetHostName("Server=localhost;Port=33060;Database=test;User=mysql;Password=mysql");
Assert.Equal("localhost", result);
}
}

23
backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/PostgresConnectionStringParserTests.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Providers.Postgres;
namespace Squidex.EntityFramework.Infrastructure.Migrations;
public class PostgresConnectionStringParserTests
{
[Fact]
public void Should_parse_host_name()
{
var sut = new PostgresConnectionStringParser();
var result = sut.GetHostName("Server=localhost;Port=54320;Database=test;User=postgres;Password=postgres");
Assert.Equal("localhost", result);
}
}

23
backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Migrations/SqlServerConnectionStringParserTests.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Providers.SqlServer;
namespace Squidex.EntityFramework.Infrastructure.Migrations;
public class SqlServerConnectionStringParserTests
{
[Fact]
public void Should_parse_host_name()
{
var sut = new SqlServerConnectionStringParser();
var result = sut.GetHostName("Server=localhost;Port=14330;Database=test;User=sa;Password=sqlserver");
Assert.Equal("localhost", result);
}
}

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

@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.App;
using Testcontainers.MySql;
@ -34,19 +35,20 @@ public class MySqlMigrationTests : IAsyncLifetime
{
var services =
new ServiceCollection()
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
var connectionString = mysql.GetConnectionString();
b.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), options =>
{
options.UseNetTopologySuite();
options.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
});
})
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<MySqlAppDbContext>>()
.BuildServiceProvider();
.AddDbContextFactory<MySqlAppDbContext>(b =>
{
var connectionString = mysql.GetConnectionString();
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>>();

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

@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.App;
using Testcontainers.PostgreSql;
@ -37,16 +38,17 @@ public class PostgresMigrationTests : IAsyncLifetime
{
var services =
new ServiceCollection()
.AddDbContextFactory<PostgresAppDbContext>(b =>
{
b.UseNpgsql(postgreSql.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<PostgresAppDbContext>>()
.BuildServiceProvider();
.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>>();

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

@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.App;
using Testcontainers.MsSql;
@ -34,16 +35,17 @@ public class SqlServerMigrationTests : IAsyncLifetime
{
var services =
new ServiceCollection()
.AddDbContextFactory<SqlServerAppDbContext>(b =>
{
b.UseSqlServer(sqlServer.GetConnectionString(), options =>
{
options.UseNetTopologySuite();
});
})
.AddSingleton(TestUtils.DefaultSerializer)
.AddSingleton<DatabaseMigrator<SqlServerAppDbContext>>()
.BuildServiceProvider();
.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>>();

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

@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.MySql;
using Squidex.Providers.MySql.Content;
using Testcontainers.MySql;
@ -58,6 +59,7 @@ public class MySqlFixture(string? reuseId = null) : IAsyncLifetime, ISqlContentF
{
return new MySqlContentDbContext(name, connectionString, null, jsonSerializer);
})
.AddSingleton<ConnectionStringParser, MySqlConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextMySql>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.BuildServiceProvider();

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

@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.Postgres;
using Squidex.Providers.Postgres.Content;
using Testcontainers.PostgreSql;
@ -57,6 +58,7 @@ public class PostgresFixture(string? reuseId) : IAsyncLifetime, ISqlContentFixtu
{
return new PostgresContentDbContext(name, connectionString, jsonSerializer);
})
.AddSingleton<ConnectionStringParser, PostgresConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextPostgres>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.BuildServiceProvider();

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

@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
using Squidex.Providers.SqlServer;
using Squidex.Providers.SqlServer.Content;
using Testcontainers.MsSql;
@ -59,6 +60,7 @@ public class SqlServerFixture(string? reuseId = null) : IAsyncLifetime, ISqlCont
{
return new SqlServerContentDbContext(name, connectionString, jsonSerializer);
})
.AddSingleton<ConnectionStringParser, SqlServerConnectionStringParser>()
.AddSingletonAs<DatabaseCreator<TestDbContextSqlServer>>().Done()
.AddSingleton(TestUtils.DefaultSerializer)
.BuildServiceProvider();

9
backend/tests/Squidex.Data.Tests/Squidex.Data.Tests.csproj

@ -9,11 +9,6 @@
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<NoWarn>SA0001;NETSDK1206</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Providers\**" />
<EmbeddedResource Remove="Providers\**" />
<None Remove="Providers\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Data.EntityFramework\Squidex.Data.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\Squidex.Data.MongoDb\Squidex.Data.MongoDb.csproj" />
@ -24,6 +19,10 @@
<ProjectReference Include="..\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -13,6 +13,10 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs

@ -8,7 +8,6 @@
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Flows;

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -20,6 +20,10 @@
<ProjectReference Include="..\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="GraphQL" Version="8.2.1" />

4
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -13,6 +13,10 @@
<ProjectReference Include="..\..\src\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">

4
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -12,6 +12,10 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">

4
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -14,6 +14,10 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="FluentAssertions" Version="[7.0.0]" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">

16
backend/tests/coverlet.runsettings.xml

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<ExcludeAssembliesWithoutSources>MissingAll</ExcludeAssembliesWithoutSources>
<Exclude>[*.Tests]*,[Squidex.Extensions]*</Exclude>
<ExcludeByAttribute>Obsolete,GeneratedCode,CompilerGenerated,ExcludeFromCodeCoverage</ExcludeByAttribute>
<ExcludeByFile>*.g.cs,**/Migrations/*.cs</ExcludeByFile>
<IncludeTestAssembly>false</IncludeTestAssembly>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>

60
backend/tests/tests.sln

@ -0,0 +1,60 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Data.Tests", "Squidex.Data.Tests\Squidex.Data.Tests.csproj", "{36FAF7D1-52B7-F128-1967-AC4160DE4528}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Data.Tests.CodeGenerator", "Squidex.Data.Tests.CodeGenerator\Squidex.Data.Tests.CodeGenerator.csproj", "{F724DB40-7C2F-4C19-7B97-77AA2268F1B5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Tests", "Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj", "{F631204E-94A4-4F1A-5011-E3255AD95AFC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Entities.Tests", "Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{EEC6B0AB-FA33-D18F-5620-A2CDDDA184A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Users.Tests", "Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj", "{F16187EF-3B10-BABF-4BEF-674DDF9177AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.Tests", "Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj", "{9C47E947-D228-B141-2B67-0D2348052F23}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Web.Tests", "Squidex.Web.Tests\Squidex.Web.Tests.csproj", "{0811601F-4F56-C9D8-3DDE-334B32B6BF7D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36FAF7D1-52B7-F128-1967-AC4160DE4528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36FAF7D1-52B7-F128-1967-AC4160DE4528}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36FAF7D1-52B7-F128-1967-AC4160DE4528}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36FAF7D1-52B7-F128-1967-AC4160DE4528}.Release|Any CPU.Build.0 = Release|Any CPU
{F724DB40-7C2F-4C19-7B97-77AA2268F1B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F724DB40-7C2F-4C19-7B97-77AA2268F1B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F724DB40-7C2F-4C19-7B97-77AA2268F1B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F724DB40-7C2F-4C19-7B97-77AA2268F1B5}.Release|Any CPU.Build.0 = Release|Any CPU
{F631204E-94A4-4F1A-5011-E3255AD95AFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F631204E-94A4-4F1A-5011-E3255AD95AFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F631204E-94A4-4F1A-5011-E3255AD95AFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F631204E-94A4-4F1A-5011-E3255AD95AFC}.Release|Any CPU.Build.0 = Release|Any CPU
{EEC6B0AB-FA33-D18F-5620-A2CDDDA184A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EEC6B0AB-FA33-D18F-5620-A2CDDDA184A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EEC6B0AB-FA33-D18F-5620-A2CDDDA184A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EEC6B0AB-FA33-D18F-5620-A2CDDDA184A1}.Release|Any CPU.Build.0 = Release|Any CPU
{F16187EF-3B10-BABF-4BEF-674DDF9177AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F16187EF-3B10-BABF-4BEF-674DDF9177AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F16187EF-3B10-BABF-4BEF-674DDF9177AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F16187EF-3B10-BABF-4BEF-674DDF9177AC}.Release|Any CPU.Build.0 = Release|Any CPU
{9C47E947-D228-B141-2B67-0D2348052F23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C47E947-D228-B141-2B67-0D2348052F23}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C47E947-D228-B141-2B67-0D2348052F23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C47E947-D228-B141-2B67-0D2348052F23}.Release|Any CPU.Build.0 = Release|Any CPU
{0811601F-4F56-C9D8-3DDE-334B32B6BF7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0811601F-4F56-C9D8-3DDE-334B32B6BF7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0811601F-4F56-C9D8-3DDE-334B32B6BF7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0811601F-4F56-C9D8-3DDE-334B32B6BF7D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A39F744B-33DA-4770-9FF1-0E73834C34ED}
EndGlobalSection
EndGlobal
Loading…
Cancel
Save