Browse Source

Net7 (#939)

* Started with main project.

* Temporary.

* Simplified type registry.

* More tests and thread safety.

* Update dockerfile and OpenApi.

* Add missing class.

* Update dependencies and OpenAPI

* Fix naming in tests

* Fix Squid/SquidMiddleware.cs

* OpenAPI fixes.

* Remove useless locks.

* Fix serializer.

* Usings simplified.

* Fix type names.

* Changelog for 7.3.

* Fix tests

* Fix tests

* Another fix for tests
pull/942/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
3d3391a783
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/check-updates.yml
  2. 16
      .github/workflows/dev.yml
  3. 12
      .github/workflows/release.yml
  4. 9
      CHANGELOG.md
  5. 4
      Dockerfile
  6. 5
      backend/.editorconfig
  7. 28
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  8. 2
      backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj
  9. 4
      backend/src/Migrations/Migrations.csproj
  10. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs
  11. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  12. 16
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  13. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs
  15. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  16. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  17. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs
  19. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs
  20. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  23. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
  24. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  25. 14
      backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  27. 4
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  28. 12
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  29. 4
      backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs
  30. 4
      backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  31. 12
      backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs
  32. 14
      backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  33. 51
      backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  34. 6
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  35. 6
      backend/src/Squidex.Domain.Users/DefaultUserService.cs
  36. 6
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  37. 14
      backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs
  38. 2
      backend/src/Squidex.Domain.Users/UserWithClaims.cs
  39. 2
      backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  40. 6
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  41. 10
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  42. 4
      backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  43. 2
      backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj
  44. 29
      backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs
  45. 14
      backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs
  46. 19
      backend/src/Squidex.Infrastructure/GravatarHelper.cs
  47. 13
      backend/src/Squidex.Infrastructure/Json/System/Constants.cs
  48. 52
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs
  49. 94
      backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs
  50. 104
      backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs
  51. 51
      backend/src/Squidex.Infrastructure/Json/System/PolymorphicTypeResolver.cs
  52. 5
      backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs
  53. 33
      backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs
  54. 17
      backend/src/Squidex.Infrastructure/NamedId{T}.cs
  55. 9
      backend/src/Squidex.Infrastructure/RandomHash.cs
  56. 53
      backend/src/Squidex.Infrastructure/Reflection/AssemblyTypeProvider.cs
  57. 2
      backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs
  58. 13
      backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs
  59. 83
      backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs
  60. 1
      backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs
  61. 24
      backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs
  62. 156
      backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs
  63. 111
      backend/src/Squidex.Infrastructure/Reflection/TypeRegistry.cs
  64. 34
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  65. 4
      backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  66. 4
      backend/src/Squidex.Shared/Squidex.Shared.csproj
  67. 9
      backend/src/Squidex.Web/Extensions.cs
  68. 108
      backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs
  69. 21
      backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs
  70. 4
      backend/src/Squidex.Web/Squidex.Web.csproj
  71. 15
      backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs
  72. 67
      backend/src/Squidex/Areas/Api/Config/OpenApi/DiscriminatorProcessor.cs
  73. 69
      backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs
  74. 30
      backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs
  75. 77
      backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs
  76. 30
      backend/src/Squidex/Areas/Api/Config/OpenApi/SchemaNameGenerator.cs
  77. 41
      backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs
  78. 2
      backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs
  79. 54
      backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs
  80. 14
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs
  81. 28
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  82. 26
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  83. 6
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs
  84. 30
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  85. 36
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  86. 14
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs
  87. 28
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs
  88. 59
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  89. 12
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  90. 37
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  91. 108
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  92. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  93. 12
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  94. 24
      backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  95. 9
      backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  96. 36
      backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  97. 11
      backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs
  98. 130
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  99. 26
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  100. 14
      backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs

2
.github/workflows/check-updates.yml

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.5.0
- uses: actions/checkout@v3
with:
token: ${{ secrets.WORKFLOW_SECRET }}

16
.github/workflows/dev.yml

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.5.0
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
@ -25,7 +25,7 @@ jobs:
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
@ -73,10 +73,10 @@ jobs:
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v2.5.0
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
@ -112,7 +112,7 @@ jobs:
- name: RUN TEST
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -126,7 +126,7 @@ jobs:
- name: RUN TEST on path
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -140,7 +140,7 @@ jobs:
- name: RUN TEST with dedicated collections
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -171,7 +171,7 @@ jobs:
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 5967))" >> $GITHUB_ENV
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2

12
.github/workflows/release.yml

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.5.0
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.5.0
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
@ -95,7 +95,7 @@ jobs:
- name: RUN TEST
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -109,7 +109,7 @@ jobs:
- name: RUN TEST on path
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -123,7 +123,7 @@ jobs:
- name: RUN TEST with dedicated collections
uses: kohlerdominik/docker-run-action@v1.1.0
with:
image: squidex/build
image: squidex/build:7
environment: |
CONFIG__BACKUPURL=http://localhost:5000
CONFIG__WAIT=60
@ -204,7 +204,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.5.0
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2

9
CHANGELOG.md

@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [7.3.0] - 2022-18-11
This version is all about the Update to .NET 7. If you host Squidex with Containers, e.g. with Docker Compose, Kubernetes or Managed Containers nothing changes. But if you host Squidex yourself in IIS or Linux, you have to ensure that you use the latest runtime.
### Changed
* **Runtime**: Migration to .NET 7
* **API**: New system how to manage type names, e.g. for field types, rule actions and so on, with the goal to improve serialization performance.
## [7.2.0] - 2022-11-11
### Fixed

4
Dockerfile

@ -1,7 +1,7 @@
#
# Stage 1, Build Backend
#
FROM mcr.microsoft.com/dotnet/sdk:6.0 as backend
FROM mcr.microsoft.com/dotnet/sdk:7.0 as backend
ARG SQUIDEX__VERSION=7.0.0
@ -68,7 +68,7 @@ RUN cp -a build /build/
#
# Stage 3, Build runtime
#
FROM mcr.microsoft.com/dotnet/aspnet:6.0.10-bullseye-slim
FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim
# Curl for debugging and libc-dev for protobuf
RUN apt-get update \

5
backend/.editorconfig

@ -13,6 +13,8 @@ insert_final_newline = true
indent_style = space
indent_size = 4
csharp_style_namespace_declarations = file_scoped
# FAILING ANALYZERS
dotnet_diagnostic.RECS0002.severity = none
dotnet_diagnostic.RECS0117.severity = none
@ -166,5 +168,8 @@ dotnet_diagnostic.SA1601.severity = none
# SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none
# SA1615: Element return value should be documented
dotnet_diagnostic.SA1615.severity = none
# SA1623: Property summary documentation should match accessors
dotnet_diagnostic.SA1623.severity = none

28
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
@ -9,29 +9,29 @@
<ProjectReference Include="..\..\src\Squidex.Web\Squidex.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="6.12.1" />
<PackageReference Include="Algolia.Search" Version="6.14.0" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.0.0-beta.3" />
<PackageReference Include="Azure.Search.Documents" Version="11.3.0" />
<PackageReference Include="Azure.Search.Documents" Version="11.4.0" />
<PackageReference Include="Confluent.Apache.Avro" Version="1.7.7.7" />
<PackageReference Include="Confluent.Kafka" Version="1.8.2" />
<PackageReference Include="Confluent.Kafka" Version="1.9.3" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="7.17.4" />
<PackageReference Include="Elasticsearch.Net" Version="7.17.5" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.4.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="3.6.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.1" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.17.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="OpenSearch.Net" Version="1.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.19.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.12.5" />
<PackageReference Include="NodaTime" Version="3.1.5" />
<PackageReference Include="OpenSearch.Net" Version="1.2.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.3.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc8" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.OpenTelemetry.Exporter.Stackdriver" Version="0.0.0-alpha.0.384" />

2
backend/i18n/translator/Squidex.Translator/Squidex.Translator.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>NU1608</NoWarn>

4
backend/src/Migrations/Migrations.csproj

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

18
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldTypeProvider.cs

@ -14,19 +14,17 @@ public class FieldTypeProvider : ITypeProvider
private const string Suffix = "Properties";
private const string SuffixOld = "FieldProperties";
public void Map(TypeNameRegistry typeNameRegistry)
public void Map(TypeRegistry typeRegistry)
{
var types = typeof(FieldTypeProvider).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract);
var addedTypes = new HashSet<Type>();
static IEnumerable<Type> FindTypes(Type baseType)
{
return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract);
}
foreach (var type in types)
foreach (var type in FindTypes(typeof(FieldProperties)))
{
if (addedTypes.Add(type))
{
typeNameRegistry.Map(type, type.TypeName(false, Suffix));
typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld));
}
typeRegistry.Add<FieldProperties>(type, type.TypeName(false, SuffixOld));
typeRegistry.Add<FieldProperties>(type, type.TypeName(false, Suffix));
}
}
}

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -12,15 +12,15 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

16
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -24,7 +24,7 @@ public sealed class RuleService : IRuleService
{
private readonly Dictionary<Type, IRuleActionHandler> ruleActionHandlers;
private readonly Dictionary<Type, IRuleTriggerHandler> ruleTriggerHandlers;
private readonly TypeNameRegistry typeNameRegistry;
private readonly TypeRegistry typeRegistry;
private readonly RuleOptions ruleOptions;
private readonly IEventEnricher eventEnricher;
private readonly IJsonSerializer serializer;
@ -39,9 +39,9 @@ public sealed class RuleService : IRuleService
IEventEnricher eventEnricher,
IJsonSerializer serializer,
ILogger<RuleService> log,
TypeNameRegistry typeNameRegistry)
TypeRegistry typeRegistry)
{
this.typeNameRegistry = typeNameRegistry;
this.typeRegistry = typeRegistry;
this.eventEnricher = eventEnricher;
this.ruleOptions = ruleOptions.Value;
this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType);
@ -295,7 +295,8 @@ public sealed class RuleService : IRuleService
private async Task<JobResult> CreateJobAsync(IRuleActionHandler actionHandler, EnrichedEvent enrichedEvent, RuleContext context, Instant now)
{
var actionName = typeNameRegistry.GetName(context.Rule.Action.GetType());
var actionType = context.Rule.Action.GetType();
var actionName = typeRegistry.GetName<RuleAction>(actionType);
var expires = now.Plus(Constants.ExpirationTime);
@ -359,17 +360,16 @@ public sealed class RuleService : IRuleService
try
{
var actionType = typeNameRegistry.GetType(actionName);
var actionType = typeRegistry.GetType<RuleAction>(actionName);
var actionHandler = ruleActionHandlers[actionType];
var deserialized = serializer.Deserialize<object>(job, actionHandler.DataType);
var actionObject = serializer.Deserialize<object>(job, actionHandler.DataType);
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
// Enforce a timeout after a configured time span.
combined.CancelAfter(GetTimeoutInMs());
result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token);
result = await actionHandler.ExecuteJobAsync(actionObject, combined.Token).WithCancellation(combined.Token);
}
}
catch (Exception ex)

31
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTypeProvider.cs

@ -206,34 +206,17 @@ public sealed class RuleTypeProvider : ITypeProvider
return type.TypeName(false, ActionSuffix, ActionSuffixV2);
}
public void Map(TypeNameRegistry typeNameRegistry)
public void Map(TypeRegistry typeRegistry)
{
foreach (var (_, actionType) in actionTypes)
{
typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
}
var addedTypes = new HashSet<Type>();
static IEnumerable<Type> FindTypes(Type baseType)
{
return baseType.Assembly.GetTypes().Where(x => baseType.IsAssignableFrom(x) && !x.IsAbstract);
}
typeRegistry.Discriminator<RuleAction>("actionType");
foreach (var type in FindTypes(typeof(EnrichedEvent)))
foreach (var (name, actionType) in actionTypes)
{
if (addedTypes.Add(type))
{
typeNameRegistry.Map(type, type.Name);
}
typeRegistry.Add<RuleAction>(actionType.Type, actionType.Type.Name);
typeRegistry.Add<RuleAction>(actionType.Type, name);
}
foreach (var type in FindTypes(typeof(RuleTrigger)))
{
if (addedTypes.Add(type))
{
typeNameRegistry.Map(type, type.Name);
}
}
typeRegistry.Map(new AssemblyTypeProvider<EnrichedEvent>());
typeRegistry.Map(new AssemblyTypeProvider<RuleTrigger>("triggerType"));
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs

@ -37,6 +37,6 @@ internal sealed class Parser
var parser = new JavaScriptParser(script, DefaultParserOptions);
return parser.ParseScript();
});
})!;
}
}

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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -21,16 +21,16 @@
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Jint" Version="3.0.0-beta-2036" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="NJsonSchema" Version="10.7.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="NJsonSchema" Version="10.8.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="4.13.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" />
</ItemGroup>

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -19,11 +19,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.17.1" />
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

4
backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Apps;
public sealed class AppHistoryEventsCreator : HistoryEventsCreatorBase
{
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
public AppHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeRegistry)
{
AddEventMessage<AppCreated>(
"history.apps.created");

6
backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs

@ -30,7 +30,7 @@ public sealed class AppPermanentDeleter : IEventConsumer
get => "^app-";
}
public AppPermanentDeleter(IEnumerable<IDeleter> deleters, IDomainObjectFactory factory, TypeNameRegistry typeNameRegistry)
public AppPermanentDeleter(IEnumerable<IDeleter> deleters, IDomainObjectFactory factory, TypeRegistry typeRegistry)
{
this.deleters = deleters.OrderBy(x => x.Order).ToList();
this.factory = factory;
@ -38,8 +38,8 @@ public sealed class AppPermanentDeleter : IEventConsumer
// Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string>
{
typeNameRegistry.GetName<AppDeleted>(),
typeNameRegistry.GetName<AppContributorRemoved>()
typeRegistry.GetName<IEvent, AppDeleted>(),
typeRegistry.GetName<IEvent, AppContributorRemoved>()
};
}

4
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs

@ -14,8 +14,8 @@ namespace Squidex.Domain.Apps.Entities.Assets;
public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase
{
public AssetHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
public AssetHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeRegistry)
{
AddEventMessage<AssetCreated>(
"history.assets.uploaded");

4
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs

@ -27,14 +27,14 @@ public sealed class AssetPermanentDeleter : IEventConsumer
get => "^asset-";
}
public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeNameRegistry typeNameRegistry)
public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeRegistry typeRegistry)
{
this.assetFileStore = assetFileStore;
// Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string>
{
typeNameRegistry.GetName<AssetDeleted>()
typeRegistry.GetName<IEvent, AssetDeleted>()
};
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker_EventHandling.cs

@ -157,7 +157,7 @@ public partial class AssetUsageTracker : IEventConsumer
// Store the latest tags in memory for fast access.
if (memoryCache.TryGetValue<State>(key, out var state))
{
return state.Tags;
return state?.Tags;
}
var stored = await store.ReadAsync(key, ct);

4
backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs

@ -37,7 +37,7 @@ public sealed class RecursiveDeleter : IEventConsumer
ICommandBus commandBus,
IAssetRepository assetRepository,
IAssetFolderRepository assetFolderRepository,
TypeNameRegistry typeNameRegistry,
TypeRegistry typeRegistry,
ILogger<RecursiveDeleter> log)
{
this.commandBus = commandBus;
@ -49,7 +49,7 @@ public sealed class RecursiveDeleter : IEventConsumer
// Compute the event types names once for performance reasons and use hashset for extensibility.
consumingTypes = new HashSet<string>
{
typeNameRegistry.GetName<AssetFolderDeleted>()
typeRegistry.GetName<IEvent, AssetFolderDeleted>()
};
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase
{
public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
public ContentHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeRegistry)
{
AddEventMessage<ContentCreated>(
"history.contents.created");

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -230,7 +230,7 @@ public class ContentQueryParser
entry.AbsoluteExpirationRelativeToNow = CacheTime;
return ContentQueryModel.Build(schema?.SchemaDef, context.App.PartitionResolver(), components);
});
})!;
return result;
}
@ -245,7 +245,7 @@ public class ContentQueryParser
entry.AbsoluteExpirationRelativeToNow = CacheTime;
return BuildQueryModel(context, schema, components).ConvertToEdm("Contents", schema?.SchemaDef.Name ?? "Generic");
});
})!;
return result;
}

14
backend/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs

@ -14,37 +14,37 @@ namespace Squidex.Domain.Apps.Entities.History;
public abstract class HistoryEventsCreatorBase : IHistoryEventsCreator
{
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private readonly TypeNameRegistry typeNameRegistry;
private readonly TypeRegistry typeRegistry;
public IReadOnlyDictionary<string, string> Texts
{
get => texts;
}
protected HistoryEventsCreatorBase(TypeNameRegistry typeNameRegistry)
protected HistoryEventsCreatorBase(TypeRegistry typeRegistry)
{
Guard.NotNull(typeNameRegistry);
Guard.NotNull(typeRegistry);
this.typeNameRegistry = typeNameRegistry;
this.typeRegistry = typeRegistry;
}
protected void AddEventMessage<TEvent>(string message) where TEvent : IEvent
{
Guard.NotNullOrEmpty(message);
texts[typeNameRegistry.GetName<TEvent>()] = message;
texts[typeRegistry.GetName<IEvent, TEvent>()] = message;
}
protected bool HasEventText(IEvent @event)
{
var message = typeNameRegistry.GetName(@event.GetType());
var message = typeRegistry.GetName<IEvent>(@event.GetType());
return texts.ContainsKey(message);
}
protected HistoryEvent ForEvent(IEvent @event, string channel)
{
var message = typeNameRegistry.GetName(@event.GetType());
var message = typeRegistry.GetName<IEvent>(@event.GetType());
return new HistoryEvent(channel, message);
}

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -111,6 +111,6 @@ public sealed class RuleEnqueuer : IEventConsumer, IRuleEnqueuer
entry.AbsoluteExpirationRelativeToNow = cacheDuration;
return appProvider.GetRulesAsync(appId);
});
})!;
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs

@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas;
public sealed class SchemaHistoryEventsCreator : HistoryEventsCreatorBase
{
public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
public SchemaHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeRegistry)
{
AddEventMessage<SchemaFieldsReordered>(
"history.schemas.fieldsReordered");

12
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
@ -22,19 +22,19 @@
<PackageReference Include="CsvHelper" Version="30.0.0" />
<PackageReference Include="GraphQL" Version="7.1.1" />
<PackageReference Include="GraphQL.DataLoader" Version="7.1.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Notifo.SDK" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Notifo.SDK" Version="1.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="9.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
<PackageReference Include="tusdotnet" Version="2.6.0" />
<PackageReference Include="tusdotnet" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />

4
backend/src/Squidex.Domain.Apps.Entities/Teams/TeamHistoryEventsCreator.cs

@ -14,8 +14,8 @@ namespace Squidex.Domain.Teams.Entities.Teams;
public sealed class TeamHistoryEventsCreator : HistoryEventsCreatorBase
{
public TeamHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
public TeamHistoryEventsCreator(TypeRegistry typeRegistry)
: base(typeRegistry)
{
AddEventMessage<TeamCreated>(
"history.teams.created");

4
backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

12
backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs

@ -62,13 +62,13 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
{
}
public async Task<IdentityRole> FindByIdAsync(string roleId,
public async Task<IdentityRole?> FindByIdAsync(string roleId,
CancellationToken cancellationToken)
{
return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken);
}
public async Task<IdentityRole> FindByNameAsync(string normalizedRoleName,
public async Task<IdentityRole?> FindByNameAsync(string normalizedRoleName,
CancellationToken cancellationToken)
{
return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken);
@ -104,19 +104,19 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
return Task.FromResult(role.Id);
}
public Task<string> GetRoleNameAsync(IdentityRole role,
public Task<string?> GetRoleNameAsync(IdentityRole role,
CancellationToken cancellationToken)
{
return Task.FromResult(role.Name);
}
public Task<string> GetNormalizedRoleNameAsync(IdentityRole role,
public Task<string?> GetNormalizedRoleNameAsync(IdentityRole role,
CancellationToken cancellationToken)
{
return Task.FromResult(role.NormalizedName);
}
public Task SetRoleNameAsync(IdentityRole role, string roleName,
public Task SetRoleNameAsync(IdentityRole role, string? roleName,
CancellationToken cancellationToken)
{
role.Name = roleName;
@ -124,7 +124,7 @@ public sealed class MongoRoleStore : MongoRepositoryBase<IdentityRole>, IRoleSto
return Task.CompletedTask;
}
public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName,
public Task SetNormalizedRoleNameAsync(IdentityRole role, string? normalizedName,
CancellationToken cancellationToken)
{
role.NormalizedName = normalizedName;

14
backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -87,18 +87,24 @@ public sealed class MongoUser : IdentityUser
claims.Foreach(RemoveClaim);
}
internal void ReplaceClaim(Claim existingClaim, Claim newClaim)
internal void ReplaceClaim(Claim existingClaim, Claim? newClaim)
{
RemoveClaim(existingClaim);
AddClaim(newClaim);
if (newClaim != null)
{
AddClaim(newClaim);
}
}
internal void ReplaceToken(string provider, string name, string value)
internal void ReplaceToken(string provider, string name, string? value)
{
RemoveToken(provider, name);
AddToken(provider, name, value);
if (value != null)
{
AddToken(provider, name, value);
}
}
}

51
backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -183,18 +183,15 @@ public sealed class MongoUserStore :
return new MongoUser { Email = email, UserName = email };
}
public async Task<IdentityUser> FindByIdAsync(string userId,
public async Task<IdentityUser?> FindByIdAsync(string userId,
CancellationToken cancellationToken)
{
if (!IsId(userId))
{
return null!;
}
var result = await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
return result;
}
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
public async Task<IdentityUser?> FindByEmailAsync(string normalizedEmail,
CancellationToken cancellationToken)
{
var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken);
@ -202,7 +199,7 @@ public sealed class MongoUserStore :
return result;
}
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
public async Task<IdentityUser?> FindByNameAsync(string normalizedUserName,
CancellationToken cancellationToken)
{
var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken);
@ -210,7 +207,7 @@ public sealed class MongoUserStore :
return result;
}
public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey,
public async Task<IdentityUser?> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken)
{
var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken);
@ -268,7 +265,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetUserNameAsync(IdentityUser user,
public Task<string?> GetUserNameAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.UserName;
@ -276,7 +273,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetNormalizedUserNameAsync(IdentityUser user,
public Task<string?> GetNormalizedUserNameAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.NormalizedUserName;
@ -284,7 +281,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetPasswordHashAsync(IdentityUser user,
public Task<string?> GetPasswordHashAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.PasswordHash;
@ -316,7 +313,7 @@ public sealed class MongoUserStore :
return Task.FromResult<IList<UserLoginInfo>>(result);
}
public Task<string> GetSecurityStampAsync(IdentityUser user,
public Task<string?> GetSecurityStampAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.SecurityStamp;
@ -324,7 +321,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetEmailAsync(IdentityUser user,
public Task<string?> GetEmailAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.Email;
@ -340,7 +337,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetNormalizedEmailAsync(IdentityUser user,
public Task<string?> GetNormalizedEmailAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.NormalizedEmail;
@ -356,7 +353,7 @@ public sealed class MongoUserStore :
return Task.FromResult<IList<Claim>>(result);
}
public Task<string> GetPhoneNumberAsync(IdentityUser user,
public Task<string?> GetPhoneNumberAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = user.PhoneNumber;
@ -404,20 +401,20 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name,
public Task<string?> GetTokenAsync(IdentityUser user, string loginProvider, string name,
CancellationToken cancellationToken)
{
var result = ((MongoUser)user).GetToken(loginProvider, name)!;
return Task.FromResult(result);
return Task.FromResult<string?>(result);
}
public Task<string> GetAuthenticatorKeyAsync(IdentityUser user,
public Task<string?> GetAuthenticatorKeyAsync(IdentityUser user,
CancellationToken cancellationToken)
{
var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!;
return Task.FromResult(result);
return Task.FromResult<string?>(result);
}
public Task<bool> HasPasswordAsync(IdentityUser user,
@ -436,7 +433,7 @@ public sealed class MongoUserStore :
return Task.FromResult(result);
}
public Task SetUserNameAsync(IdentityUser user, string userName,
public Task SetUserNameAsync(IdentityUser user, string? userName,
CancellationToken cancellationToken)
{
((MongoUser)user).UserName = userName;
@ -444,7 +441,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,
public Task SetNormalizedUserNameAsync(IdentityUser user, string? normalizedName,
CancellationToken cancellationToken)
{
((MongoUser)user).NormalizedUserName = normalizedName;
@ -452,7 +449,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetPasswordHashAsync(IdentityUser user, string passwordHash,
public Task SetPasswordHashAsync(IdentityUser user, string? passwordHash,
CancellationToken cancellationToken)
{
((MongoUser)user).PasswordHash = passwordHash;
@ -500,7 +497,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetEmailAsync(IdentityUser user, string email,
public Task SetEmailAsync(IdentityUser user, string? email,
CancellationToken cancellationToken)
{
((MongoUser)user).Email = email;
@ -516,7 +513,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,
public Task SetNormalizedEmailAsync(IdentityUser user, string? normalizedEmail,
CancellationToken cancellationToken)
{
((MongoUser)user).NormalizedEmail = normalizedEmail;
@ -548,7 +545,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber,
public Task SetPhoneNumberAsync(IdentityUser user, string? phoneNumber,
CancellationToken cancellationToken)
{
((MongoUser)user).PhoneNumber = phoneNumber;
@ -604,7 +601,7 @@ public sealed class MongoUserStore :
return Task.CompletedTask;
}
public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value,
public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string? value,
CancellationToken cancellationToken)
{
((MongoUser)user).ReplaceToken(loginProvider, name, value);

6
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -19,12 +19,12 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.17.1" />
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />

6
backend/src/Squidex.Domain.Users/DefaultUserService.cs

@ -46,7 +46,7 @@ public sealed class DefaultUserService : IUserService
{
Guard.NotNull(user);
return userManager.GetUserId(user);
return userManager.GetUserId(user)!;
}
public async Task<IResultList<IUser>> QueryAsync(IEnumerable<string> ids,
@ -82,7 +82,7 @@ public sealed class DefaultUserService : IUserService
{
var normalizedEmail = userManager.NormalizeEmail(email);
result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail));
result = result.Where(x => x.NormalizedEmail!.Contains(normalizedEmail));
}
return result;
@ -387,7 +387,7 @@ public sealed class DefaultUserService : IUserService
if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase)))
{
claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email));
claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email!));
}
return new UserWithClaims(user, claims.ToList());

6
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -18,11 +18,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityModel" Version="6.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="7.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

14
backend/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs

@ -25,17 +25,21 @@ public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFac
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
var principal = await base.CreateAsync(user);
var userPrincipal = await base.CreateAsync(user);
var userIdentity = userPrincipal.Identities.First();
var identity = principal.Identities.First();
var email = await UserManager.GetEmailAsync(user);
identity.AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user)));
if (email != null)
{
userIdentity.AddClaim(new Claim(OpenIdClaims.Email, email));
}
if (await UserManager.IsInRoleAsync(user, AdministratorRole))
{
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin));
userIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, PermissionIds.Admin));
}
return principal;
return userPrincipal;
}
}

2
backend/src/Squidex.Domain.Users/UserWithClaims.cs

@ -22,7 +22,7 @@ internal sealed class UserWithClaims : IUser
public string Email
{
get => Identity.Email;
get => Identity.Email!;
}
public bool IsLocked

2
backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>

6
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -14,8 +14,8 @@
<PackageReference Include="EventStore.Client.Grpc.PersistentSubscriptions" Version="22.0.0" />
<PackageReference Include="EventStore.Client.Grpc.ProjectionManagement" Version="22.0.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="22.0.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.46.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

10
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -14,15 +14,15 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MongoDB.Driver" Version="2.17.1" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.17.1" />
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.18.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>

4
backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -11,7 +11,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

2
backend/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>

29
backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs

@ -14,11 +14,11 @@ namespace Squidex.Infrastructure.EventSourcing;
public sealed class DefaultEventFormatter : IEventFormatter
{
private readonly IJsonSerializer serializer;
private readonly TypeNameRegistry typeNameRegistry;
private readonly TypeRegistry typeRegistry;
public DefaultEventFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer)
public DefaultEventFormatter(TypeRegistry typeRegistry, IJsonSerializer serializer)
{
this.typeNameRegistry = typeNameRegistry;
this.typeRegistry = typeRegistry;
this.serializer = serializer;
}
@ -33,7 +33,8 @@ public sealed class DefaultEventFormatter : IEventFormatter
if (envelope == null)
{
throw new TypeNameNotFoundException($"Cannot find event with type name '{storedEvent.Data.Type}'.");
ThrowHelper.InvalidOperationException($"Cannot find event with type name '{storedEvent.Data.Type}'.");
return default!;
}
return envelope;
@ -43,21 +44,19 @@ public sealed class DefaultEventFormatter : IEventFormatter
{
Guard.NotNull(storedEvent);
var payloadType = typeNameRegistry.GetTypeOrNull(storedEvent.Data.Type);
if (payloadType == null)
if (!typeRegistry.TryGetType<IEvent>(storedEvent.Data.Type, out var type))
{
return null;
}
var payloadValue = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, payloadType);
var payload = serializer.Deserialize<IEvent>(storedEvent.Data.Payload, type);
if (payloadValue is IMigrated<IEvent> migratedEvent)
if (payload is IMigrated<IEvent> migratedEvent)
{
payloadValue = migratedEvent.Migrate();
payload = migratedEvent.Migrate();
}
var envelope = new Envelope<IEvent>(payloadValue, storedEvent.Data.Headers);
var envelope = new Envelope<IEvent>(payload, storedEvent.Data.Headers);
envelope.SetEventPosition(storedEvent.EventPosition);
envelope.SetEventStreamNumber(storedEvent.EventStreamNumber);
@ -67,14 +66,14 @@ public sealed class DefaultEventFormatter : IEventFormatter
public EventData ToEventData(Envelope<IEvent> envelope, Guid commitId, bool migrate = true)
{
var payloadValue = envelope.Payload;
var payload = envelope.Payload;
if (migrate && payloadValue is IMigrated<IEvent> migratedEvent)
if (migrate && payload is IMigrated<IEvent> migratedEvent)
{
payloadValue = migratedEvent.Migrate();
payload = migratedEvent.Migrate();
}
var payloadType = typeNameRegistry.GetName(payloadValue.GetType());
var payloadType = typeRegistry.GetName<IEvent>(payload.GetType());
var payloadJson = serializer.Serialize(envelope.Payload, envelope.Payload.GetType());
envelope.SetCommitId(commitId);

14
backend/src/Squidex.Infrastructure/EventSourcing/EventTypeAttribute.cs

@ -12,6 +12,8 @@ namespace Squidex.Infrastructure.EventSourcing;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class EventTypeAttribute : TypeNameAttribute
{
private const string Suffix = "Event";
public EventTypeAttribute(string typeName, int version = 1)
: base(CreateTypeName(typeName, version))
{
@ -19,6 +21,16 @@ public sealed class EventTypeAttribute : TypeNameAttribute
private static string CreateTypeName(string typeName, int version)
{
return $"{typeName}Event" + (version > 1 ? $"V{version}" : string.Empty);
if (!typeName.EndsWith(Suffix, StringComparison.OrdinalIgnoreCase))
{
typeName += Suffix;
}
if (version > 1)
{
typeName = $"{typeName}V{version}";
}
return typeName;
}
}

19
backend/src/Squidex.Infrastructure/GravatarHelper.cs

@ -29,19 +29,16 @@ public static class GravatarHelper
private static string Hash(string email)
{
using (var md5 = MD5.Create())
{
var normalizedEmail = email.ToLowerInvariant().Trim();
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(normalizedEmail));
var hashBuilder = new StringBuilder();
var normalizedEmail = email.ToLowerInvariant().Trim();
for (var i = 0; i < hashBytes.Length; i++)
{
hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture));
}
var hashBytes = MD5.HashData(Encoding.UTF8.GetBytes(normalizedEmail));
var hashBuilder = new StringBuilder();
return hashBuilder.ToString();
for (var i = 0; i < hashBytes.Length; i++)
{
hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture));
}
return hashBuilder.ToString();
}
}

13
backend/src/Squidex.Infrastructure/Reflection/AutoAssembyTypeProvider.cs → backend/src/Squidex.Infrastructure/Json/System/Constants.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection;
using System.Text.Json;
public sealed class AutoAssembyTypeProvider<T> : ITypeProvider
namespace Squidex.Infrastructure.Json.System;
internal sealed class Constants
{
public void Map(TypeNameRegistry typeNameRegistry)
{
typeNameRegistry.MapUnmapped(typeof(T).Assembly);
}
public const string DefaultDiscriminatorProperty = "$type";
public static readonly JsonEncodedText DefaultDiscriminiatorPropertyJson = JsonEncodedText.Encode("$type");
}

52
backend/src/Squidex.Infrastructure/Json/System/InheritanceConverter.cs

@ -1,52 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class InheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull
{
private readonly TypeNameRegistry typeNameRegistry;
public InheritanceConverter(TypeNameRegistry typeNameRegistry)
: base("$type")
{
this.typeNameRegistry = typeNameRegistry;
}
public override Type GetDiscriminatorType(string name, Type typeToConvert)
{
var typeInfo = typeNameRegistry.GetTypeOrNull(name);
if (typeInfo == null)
{
typeInfo = Type.GetType(name);
}
if (typeInfo == null)
{
ThrowHelper.JsonException($"Object has invalid discriminator '{name}'.");
return default!;
}
return typeInfo;
}
public override string GetDiscriminatorValue(Type type)
{
var typeName = typeNameRegistry.GetNameOrNull(type);
if (typeName == null)
{
// Use the type name as a fallback.
typeName = type.AssemblyQualifiedName!;
}
return typeName;
}
}

94
backend/src/Squidex.Infrastructure/Json/System/InheritanceConverterBase.cs

@ -1,94 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Infrastructure.Json.System;
public abstract class InheritanceConverterBase<T> : JsonConverter<T> where T : notnull
{
private readonly JsonEncodedText discriminatorProperty;
public string DiscriminatorName { get; }
protected InheritanceConverterBase(string discriminatorName)
{
discriminatorProperty = JsonEncodedText.Encode(discriminatorName);
DiscriminatorName = discriminatorName;
}
public abstract Type GetDiscriminatorType(string name, Type typeToConvert);
public abstract string GetDiscriminatorValue(Type type);
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var propertyName = typeReader.GetString();
if (typeReader.Read() && typeReader.TokenType == JsonTokenType.String && propertyName == DiscriminatorName)
{
var type = GetDiscriminatorType(typeReader.GetString()!, typeToConvert);
return (T?)JsonSerializer.Deserialize(ref reader, type, options);
}
else
{
using var document = JsonDocument.ParseValue(ref reader);
if (!document.RootElement.TryGetProperty(DiscriminatorName, out var discriminator))
{
ThrowHelper.JsonException($"Object has no discriminator '{DiscriminatorName}.");
return default!;
}
var type = GetDiscriminatorType(discriminator.GetString()!, typeToConvert);
using var bufferWriter = DefaultPools.MemoryStream.GetStream();
using (var writer = new Utf8JsonWriter(bufferWriter))
{
document.RootElement.WriteTo(writer);
}
return (T?)JsonSerializer.Deserialize(bufferWriter.ToArray(), type, options);
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var name = GetDiscriminatorValue(value.GetType());
writer.WriteStartObject();
writer.WriteString(discriminatorProperty, name);
using (var document = JsonSerializer.SerializeToDocument(value, value.GetType(), options))
{
foreach (var property in document.RootElement.EnumerateObject())
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}

104
backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs

@ -0,0 +1,104 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
{
private readonly JsonEncodedText discriminatorProperty;
private readonly string discriminatorName;
private readonly TypeRegistry typeRegistry;
public PolymorphicConverter(TypeRegistry typeRegistry)
{
this.typeRegistry = typeRegistry;
typeRegistry.TryGetConfig<T>(out var config);
discriminatorName = config?.DiscriminatorProperty ?? Constants.DefaultDiscriminatorProperty;
discriminatorProperty = JsonEncodedText.Encode(discriminatorName);
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Creating a copy of the reader (The derived deserialisation has to be done from the start)
Utf8JsonReader typeReader = reader;
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (typeReader.Read())
{
if (typeReader.TokenType == JsonTokenType.PropertyName && IsDiscriminiator(typeReader))
{
// Advance the reader to the property value
typeReader.Read();
if (typeReader.TokenType != JsonTokenType.String)
{
ThrowHelper.JsonException($"Expected string discriminator value, got '{reader.TokenType}'");
return default!;
}
// Resolve the type from the discriminator value.
var type = GetDiscriminatorType(typeReader.GetString()!);
// Perform the actual deserialization with the original reader
return (T)JsonSerializer.Deserialize(ref reader, type, options)!;
}
else if (typeReader.TokenType == JsonTokenType.StartObject || typeReader.TokenType == JsonTokenType.StartArray)
{
if (!typeReader.TrySkip())
{
typeReader.Skip();
}
}
}
ThrowHelper.JsonException($"Object has no discriminator '{discriminatorName}.");
return default!;
}
private bool IsDiscriminiator(Utf8JsonReader typeReader)
{
return
typeReader.ValueTextEquals(discriminatorProperty.EncodedUtf8Bytes) ||
typeReader.ValueTextEquals(Constants.DefaultDiscriminatorProperty);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
EnsureTypeResolver(options);
JsonSerializer.Serialize<object>(writer, value!, options);
}
private static void EnsureTypeResolver(JsonSerializerOptions options)
{
if (options.TypeInfoResolver is not PolymorphicTypeResolver)
{
ThrowHelper.JsonException($"TypeInfoResolver must be of type PolymorphicTypeResolver.");
}
}
private Type GetDiscriminatorType(string name)
{
if (!typeRegistry.TryGetType<T>(name, out var type))
{
ThrowHelper.JsonException($"Object has invalid discriminator '{name}'.");
return default!;
}
return type;
}
}

51
backend/src/Squidex.Infrastructure/Json/System/PolymorphicTypeResolver.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.Json.System;
public sealed class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
private readonly TypeRegistry typeRegistry;
public PolymorphicTypeResolver(TypeRegistry typeRegistry)
{
Guard.NotNull(typeRegistry);
this.typeRegistry = typeRegistry;
}
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var typeInfo = base.GetTypeInfo(type, options);
var baseType = type.BaseType;
while (baseType != null)
{
if (typeRegistry.TryGetConfig(baseType, out var config) && config.TryGetName(type, out var typeName))
{
var discriminiatorName = config.DiscriminatorProperty ?? Constants.DefaultDiscriminatorProperty;
var discriminatorField = typeInfo.CreateJsonPropertyInfo(typeof(string), discriminiatorName);
discriminatorField.Get = x =>
{
return typeName;
};
typeInfo.Properties.Insert(0, discriminatorField);
}
baseType = baseType.BaseType;
}
return typeInfo;
}
}

5
backend/src/Squidex.Infrastructure/Json/System/ReflectionHelper.cs

@ -42,4 +42,9 @@ internal static class ReflectionHelper
return dynamicMethod;
}
public static string GetShortTypeName(this Type type)
{
return $"{type.FullName}, {type.Assembly.GetName().Name}";
}
}

33
backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -47,10 +48,7 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
}
case JsonTokenType.StartObject:
var optionsWithoutSelf = new JsonSerializerOptions(options);
// Remove the current converter, otherwise we would create a stackoverflow exception.
optionsWithoutSelf.Converters.Remove(this);
var optionsWithoutSelf = OptionClones.GetOptionsWithoutConverter(options, this);
return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf);
@ -75,3 +73,30 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
writer.WritePropertyName(convertToString(value)!);
}
}
#pragma warning disable MA0048 // File name must match type name
internal static class OptionClones
#pragma warning restore MA0048 // File name must match type name
{
private static readonly ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> Clones = new ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions>();
public static JsonSerializerOptions GetOptionsWithoutConverter(JsonSerializerOptions source, JsonConverter converter)
{
if (!source.Converters.Contains(converter))
{
return source;
}
if (!Clones.TryGetValue(source, out var clone))
{
clone = new JsonSerializerOptions(source);
// Remove the current converter, otherwise we would create a stackoverflow exception.
clone.Converters.Remove(converter);
Clones.TryAdd(source, clone);
}
return clone;
}
}

17
backend/src/Squidex.Infrastructure/NamedId{T}.cs

@ -9,29 +9,18 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
#pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Infrastructure;
public delegate bool Parser<T>(ReadOnlySpan<char> input, out T result);
[TypeConverter(typeof(NamedIdTypeConverter))]
public sealed record NamedId<T> where T : notnull
public sealed record NamedId<T>(T Id, string Name) where T : notnull
{
private static readonly int GuidLength = Guid.Empty.ToString().Length;
public T Id { get; }
public string Name { get; }
public NamedId(T id, string name)
{
Guard.NotNull(id);
Guard.NotNull(name);
Id = id;
Name = name;
}
public string Name { get; init; } = Guard.NotNullOrEmpty(Name);
public override string ToString()
{

9
backend/src/Squidex.Infrastructure/RandomHash.cs

@ -34,14 +34,11 @@ public static class RandomHash
public static string ToSha256Base64(this byte[] bytes)
{
using (var sha = SHA256.Create())
{
var bytesHash = sha.ComputeHash(bytes);
var hashBytes = SHA256.HashData(bytes);
var result = Convert.ToBase64String(bytesHash);
var result = Convert.ToBase64String(hashBytes);
return result;
}
return result;
}
public static string ToSha256(this string value)

53
backend/src/Squidex.Infrastructure/Reflection/AssemblyTypeProvider.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
namespace Squidex.Infrastructure.Reflection;
public sealed class AssemblyTypeProvider<T> : ITypeProvider where T : class
{
private readonly Assembly assembly;
public string? DiscriminatorProperty { get; }
public AssemblyTypeProvider(string? discriminatorProperty = null)
: this(typeof(T).Assembly, discriminatorProperty)
{
}
public AssemblyTypeProvider(Assembly assembly, string? discriminatorProperty = null)
{
Guard.NotNull(assembly);
this.assembly = assembly;
DiscriminatorProperty = discriminatorProperty;
}
public void Map(TypeRegistry typeRegistry)
{
var baseType = typeof(T);
foreach (var derivedType in assembly.GetTypes())
{
if (derivedType.IsAssignableTo(baseType) && !derivedType.IsAbstract)
{
var typeName = derivedType.GetCustomAttribute<TypeNameAttribute>()?.TypeName;
if (string.IsNullOrWhiteSpace(typeName))
{
typeName = derivedType.TypeName(false, baseType.Name);
}
typeRegistry.Add<T>(derivedType, typeName);
}
}
typeRegistry.Discriminator<T>(DiscriminatorProperty);
}
}

2
backend/src/Squidex.Infrastructure/Reflection/ITypeProvider.cs

@ -9,5 +9,5 @@ namespace Squidex.Infrastructure.Reflection;
public interface ITypeProvider
{
void Map(TypeNameRegistry typeNameRegistry);
void Map(TypeRegistry typeRegistry);
}

13
backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs

@ -1,13 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection;
[AttributeUsage(AttributeTargets.Property)]
public sealed class IgnoreEqualsAttribute : Attribute
{
}

83
backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs

@ -0,0 +1,83 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeConfig
{
private readonly List<(Type DerivedType, string TypeName)> derivedTypes = new List<(Type DervicedType, string TypeName)>();
private Dictionary<string, Type>? mapByName;
private Dictionary<Type, string>? mapByType;
public string? DiscriminatorProperty { get; set; }
public IReadOnlyList<(Type DerivedType, string TypeName)> DerivedTypes
{
get => derivedTypes;
}
internal void Add(Type derivedType, string typeName)
{
Guard.NotNull(derivedType);
Guard.NotNullOrEmpty(typeName);
if (!derivedTypes.Contains((derivedType, typeName)))
{
derivedTypes.Add((derivedType, typeName));
}
var (conflict, _) = derivedTypes.Find(x => x.TypeName == typeName && x.DerivedType != derivedType);
if (conflict != null)
{
ThrowHelper.ArgumentException($"Type name '{typeName}' is already used by type '{conflict}", nameof(typeName));
}
mapByName = null;
mapByType = null;
}
public bool TryGetType(string typeName, [MaybeNullWhen(false)] out Type derivedType)
{
var map = mapByName ??= BuildMapByName();
return map.TryGetValue(typeName, out derivedType);
}
public bool TryGetName(Type derivedType, [MaybeNullWhen(false)] out string typeName)
{
var map = mapByType ??= BuildMapByType();
return map.TryGetValue(derivedType, out typeName);
}
private Dictionary<string, Type> BuildMapByName()
{
var result = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
foreach (var (derivedType, typeName) in derivedTypes)
{
result[typeName] = derivedType;
}
return result;
}
private Dictionary<Type, string> BuildMapByType()
{
var result = new Dictionary<Type, string>();
foreach (var (derivedType, typeName) in derivedTypes)
{
result[derivedType] = typeName;
}
return result;
}
}

1
backend/src/Squidex.Infrastructure/Reflection/TypeNameBuilder.cs

@ -22,7 +22,6 @@ public static class TypeNameBuilder
if (typeName.EndsWith(suffix, StringComparison.Ordinal))
{
typeName = typeName[..^suffix.Length];
break;
}
}

24
backend/src/Squidex.Infrastructure/Reflection/TypeNameNotFoundException.cs

@ -1,24 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Runtime.Serialization;
namespace Squidex.Infrastructure.Reflection;
[Serializable]
public class TypeNameNotFoundException : Exception
{
public TypeNameNotFoundException(string? message = null, Exception? inner = null)
: base(message, inner)
{
}
protected TypeNameNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}

156
backend/src/Squidex.Infrastructure/Reflection/TypeNameRegistry.cs

@ -1,156 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeNameRegistry
{
private readonly Dictionary<Type, string> namesByType = new Dictionary<Type, string>();
private readonly Dictionary<string, Type> typesByName = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
public TypeNameRegistry(IEnumerable<ITypeProvider>? providers = null)
{
if (providers != null)
{
foreach (var provider in providers)
{
Map(provider);
}
}
}
public TypeNameRegistry MapObsolete(Type type, string name)
{
Guard.NotNull(type);
Guard.NotNull(name);
lock (namesByType)
{
if (typesByName.TryGetValue(name, out var existingType) && existingType != type)
{
var message = $"The name '{name}' is already registered with type '{typesByName[name]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
typesByName[name] = type;
}
return this;
}
public TypeNameRegistry Map(ITypeProvider provider)
{
Guard.NotNull(provider);
provider.Map(this);
return this;
}
public TypeNameRegistry Map(Type type)
{
Guard.NotNull(type);
var typeNameAttribute = type.GetCustomAttribute<TypeNameAttribute>();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
{
Map(type, typeNameAttribute.TypeName);
}
return this;
}
public TypeNameRegistry Map(Type type, string name)
{
Guard.NotNull(type);
Guard.NotNull(name);
lock (namesByType)
{
if (namesByType.TryGetValue(type, out var existingName) && existingName != name)
{
var message = $"The type '{type}' is already registered with name '{namesByType[type]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
namesByType[type] = name;
if (typesByName.TryGetValue(name, out var existingType) && existingType != type)
{
var message = $"The name '{name}' is already registered with type '{typesByName[name]}'";
ThrowHelper.ArgumentException(message, nameof(type));
}
typesByName[name] = type;
}
return this;
}
public TypeNameRegistry MapUnmapped(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
if (!namesByType.ContainsKey(type))
{
Map(type);
}
}
return this;
}
public string GetName<T>()
{
return GetName(typeof(T));
}
public string? GetNameOrNull<T>()
{
return GetNameOrNull(typeof(T));
}
public string? GetNameOrNull(Type type)
{
return namesByType.GetValueOrDefault(type);
}
public Type? GetTypeOrNull(string name)
{
return typesByName.GetValueOrDefault(name);
}
public string GetName(Type type)
{
var result = namesByType.GetValueOrDefault(type);
if (result == null)
{
throw new TypeNameNotFoundException($"There is no name for type '{type}");
}
return result;
}
public Type GetType(string name)
{
var result = typesByName.GetValueOrDefault(name);
if (result == null)
{
throw new TypeNameNotFoundException($"There is no type for name '{name}");
}
return result;
}
}

111
backend/src/Squidex.Infrastructure/Reflection/TypeRegistry.cs

@ -0,0 +1,111 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Reflection;
public sealed class TypeRegistry
{
private readonly Dictionary<Type, TypeConfig> configs = new Dictionary<Type, TypeConfig>();
public TypeRegistry(IEnumerable<ITypeProvider>? providers = null)
{
if (providers != null)
{
foreach (var provider in providers)
{
Map(provider);
}
}
}
public TypeRegistry Map(ITypeProvider provider)
{
Guard.NotNull(provider);
provider.Map(this);
return this;
}
public TypeRegistry Add<TBase, TDerived>(string typeName) where TDerived : TBase where TBase : class
{
return Add<TBase>(typeof(TDerived), typeName);
}
public TypeRegistry Add<T>(Type derivedType, string typeName) where T : class
{
lock (configs)
{
configs.GetOrAddNew(typeof(T)).Add(derivedType, typeName);
}
return this;
}
public TypeRegistry Discriminator<T>(string? discriminiatorProperty)
{
lock (configs)
{
configs.GetOrAddNew(typeof(T)).DiscriminatorProperty ??= discriminiatorProperty;
}
return this;
}
public string GetName<TBase, TDerived>() where TDerived : TBase where TBase : class
{
return GetName<TBase>(typeof(TDerived));
}
public string GetName<T>(Type derivedType) where T : class
{
if (!TryGetName<T>(derivedType, out var name))
{
ThrowHelper.ArgumentException($"Unknown derived type {derivedType}.", nameof(derivedType));
return default!;
}
return name;
}
public Type GetType<T>(string typeName) where T : class
{
if (!TryGetType<T>(typeName, out var name))
{
ThrowHelper.ArgumentException($"Unknown derived type {typeName}.", nameof(typeName));
return default!;
}
return name;
}
public bool TryGetName<T>(Type derivedType, [MaybeNullWhen(false)] out string typeName) where T : class
{
typeName = null!;
return TryGetConfig<T>(out var config) && config.TryGetName(derivedType, out typeName);
}
public bool TryGetType<T>(string typeName, [MaybeNullWhen(false)] out Type derivedType) where T : class
{
derivedType = null!;
return TryGetConfig<T>(out var config) && config.TryGetType(typeName, out derivedType);
}
public bool TryGetConfig<T>([MaybeNullWhen(false)] out TypeConfig config) where T : class
{
return TryGetConfig(typeof(T), out config);
}
public bool TryGetConfig(Type baseType, [MaybeNullWhen(false)] out TypeConfig config)
{
return configs.TryGetValue(baseType, out config);
}
}

34
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
@ -11,33 +11,33 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="3.2.0" />
<PackageReference Include="MailKit" Version="3.4.2" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="6.0.5" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.11.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="OpenTelemetry.Api" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.12.5" />
<PackageReference Include="NodaTime" Version="3.1.5" />
<PackageReference Include="OpenTelemetry.Api" Version="1.3.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="4.13.0" />
<PackageReference Include="Squidex.Caching" Version="4.13.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="4.13.0" />
<PackageReference Include="Squidex.Log" Version="4.13.0" />
<PackageReference Include="Squidex.Messaging" Version="4.13.0" />
<PackageReference Include="Squidex.Text" Version="4.13.0" />
<PackageReference Include="Squidex.Assets" Version="5.0.0" />
<PackageReference Include="Squidex.Caching" Version="5.0.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Squidex.Log" Version="5.0.0" />
<PackageReference Include="Squidex.Messaging" Version="5.0.0" />
<PackageReference Include="Squidex.Text" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />

4
backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -67,7 +67,7 @@ public sealed class CachingUsageTracker : IUsageTracker
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return inner.GetForMonthAsync(key, date, category, ct);
});
})!;
}
public Task<Counters> GetAsync(string key, DateTime fromDate, DateTime toDate, string? category,
@ -82,6 +82,6 @@ public sealed class CachingUsageTracker : IUsageTracker
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return inner.GetAsync(key, fromDate, toDate, category, ct);
});
})!;
}
}

4
backend/src/Squidex.Shared/Squidex.Shared.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
@ -9,7 +9,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

9
backend/src/Squidex.Web/Extensions.cs

@ -8,7 +8,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Squidex.Infrastructure.Security;
namespace Squidex.Web;
@ -62,11 +61,13 @@ public static class Extensions
{
result = null!;
if (headers.TryGetValue(header, out var value) && value != StringValues.Empty)
if (headers.TryGetValue(header, out var value))
{
if (!string.IsNullOrWhiteSpace(value))
string? valueString = value;
if (!string.IsNullOrWhiteSpace(valueString))
{
result = value;
result = valueString;
return true;
}
}

108
backend/src/Squidex.Web/Json/JsonInheritanceConverter.cs

@ -1,108 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
using System.Runtime.Serialization;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.System;
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Web.Json;
public class JsonInheritanceConverter<T> : InheritanceConverterBase<T> where T : notnull
{
private static readonly Lazy<Dictionary<string, Type>> DefaultMapping = new Lazy<Dictionary<string, Type>>(() =>
{
var baseName = typeof(T).Name;
var result = new Dictionary<string, Type>();
void AddType(Type type)
{
var typeName = type.Name;
if (typeName.EndsWith(baseName, StringComparison.CurrentCulture))
{
typeName = typeName[..^baseName.Length];
}
result[typeName] = type;
}
foreach (var attribute in typeof(T).GetCustomAttributes<KnownTypeAttribute>())
{
if (attribute.Type != null)
{
if (!attribute.Type.IsAbstract)
{
AddType(attribute.Type);
}
}
else if (!string.IsNullOrWhiteSpace(attribute.MethodName))
{
var method = typeof(T).GetMethod(attribute.MethodName);
if (method != null && method.IsStatic)
{
var types = (IEnumerable<Type>)method.Invoke(null, Array.Empty<object>())!;
foreach (var type in types)
{
if (!type.IsAbstract)
{
AddType(type);
}
}
}
}
}
return result;
});
private readonly IReadOnlyDictionary<string, Type> mapping;
public JsonInheritanceConverter()
: this(null, DefaultMapping.Value)
{
}
public JsonInheritanceConverter(string? discriminatorName)
: this(discriminatorName, DefaultMapping.Value)
{
}
public JsonInheritanceConverter(string? discriminatorName, IReadOnlyDictionary<string, Type> mapping)
: base(GetDiscriminatorName(discriminatorName))
{
this.mapping = mapping ?? DefaultMapping.Value;
}
private static string GetDiscriminatorName(string? discriminatorName)
{
var attribute = typeof(T).GetCustomAttribute<JsonInheritanceConverterAttribute>();
return attribute?.DiscriminatorName ?? discriminatorName ?? "discriminator";
}
public override Type GetDiscriminatorType(string name, Type typeToConvert)
{
if (!mapping.TryGetValue(name, out var type))
{
ThrowHelper.JsonException($"Could not find subtype of '{typeToConvert.Name}' with discriminator '{name}'.");
return default!;
}
return type;
}
public override string GetDiscriminatorValue(Type type)
{
return mapping.FirstOrDefault(x => x.Value == type).Key ?? type.Name;
}
}

21
backend/src/Squidex.Web/Json/JsonInheritanceConverterAttribute.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.Json.Serialization;
namespace Squidex.Web.Json;
public sealed class JsonInheritanceConverterAttribute : JsonConverterAttribute
{
public string DiscriminatorName { get; }
public JsonInheritanceConverterAttribute(Type baseType, string discriminatorName = "$type")
: base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType))
{
DiscriminatorName = discriminatorName;
}
}

4
backend/src/Squidex.Web/Squidex.Web.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -16,7 +16,7 @@
<PackageReference Include="GraphQL" Version="7.1.1" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.1.1" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="7.1.1" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702">
<PackageReference Include="Meziantou.Analyzer" Version="1.0.746">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

15
backend/src/Squidex/Areas/Api/Config/OpenApi/CommonProcessor.cs

@ -16,14 +16,9 @@ namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class CommonProcessor : IDocumentProcessor
{
private readonly string version;
private readonly string logoBackground = "#3f83df";
private readonly string logoColor = "#3f83df";
private readonly string logoUrl;
private readonly OpenApiExternalDocumentation documentation = new OpenApiExternalDocumentation
{
Url = "https://docs.squidex.io"
};
public CommonProcessor(ExposedValues exposedValues, IUrlGenerator urlGenerator)
{
logoUrl = urlGenerator.BuildUrl("images/logo-white.png", false);
@ -36,6 +31,7 @@ public sealed class CommonProcessor : IDocumentProcessor
public void Process(DocumentProcessorContext context)
{
context.Document.Info.Title = "Squidex API";
context.Document.Info.Version = version;
context.Document.Info.ExtensionData = new Dictionary<string, object>
{
@ -43,10 +39,13 @@ public sealed class CommonProcessor : IDocumentProcessor
{
url = logoUrl,
backgroundStyle = string.Empty,
backgroundColor = logoBackground
backgroundColor = logoColor
}
};
context.Document.ExternalDocumentation = documentation;
context.Document.ExternalDocumentation = new OpenApiExternalDocumentation
{
Url = "https://docs.squidex.io"
};
}
}

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

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NJsonSchema.Generation;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class DiscriminatorProcessor : ISchemaProcessor
{
private readonly TypeRegistry typeRegistry;
public DiscriminatorProcessor(TypeRegistry typeRegistry)
{
this.typeRegistry = typeRegistry;
}
public void Process(SchemaProcessorContext context)
{
if (context.Schema.DiscriminatorObject != null)
{
return;
}
if (!typeRegistry.TryGetConfig(context.ContextualType.Type, out var config) ||
config.DerivedTypes.Count <= 0 ||
config.DiscriminatorProperty == null)
{
return;
}
var discriminatorName = config.DiscriminatorProperty;
var discriminatorObject = new OpenApiDiscriminator
{
PropertyName = discriminatorName
};
var schema = context.Schema;
foreach (var (derivedType, typeName) in config.DerivedTypes)
{
var derivedSchema = context.Generator.Generate(derivedType, context.Resolver);
discriminatorObject.Mapping[typeName] = new JsonSchema
{
Reference = derivedSchema
};
}
schema.DiscriminatorObject = discriminatorObject;
if (!schema.Properties.TryGetValue(discriminatorName, out var existingProperty))
{
schema.Properties[discriminatorName] = existingProperty = new JsonSchemaProperty
{
Type = JsonObjectType.String
};
}
existingProperty.IsRequired = true;
}
}

69
backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Xml.Linq;
using Namotion.Reflection;
using NJsonSchema;
using NSwag;
@ -14,54 +15,68 @@ using Squidex.Web;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class ErrorDtoProcessor : IDocumentProcessor
public sealed class ErrorDtoProcessor : IOperationProcessor
{
public void Process(DocumentProcessorContext context)
public bool Process(OperationProcessorContext context)
{
var errorSchema = GetErrorSchema(context);
var operation = context.OperationDescription.Operation;
foreach (var operation in context.Document.Paths.Values.SelectMany(x => x.Values))
void AddResponse(string code, string description)
{
AddErrorResponses(operation, errorSchema);
if (!IsErrorCode(code))
{
return;
}
CleanupResponses(operation);
if (!operation.Responses.ContainsKey(code))
{
operation.Responses.Add(code, new OpenApiResponse
{
Description = description
});
}
}
}
private static void AddErrorResponses(OpenApiOperation operation, JsonSchema errorSchema)
{
if (!operation.Responses.ContainsKey("500"))
{
const string description = "Operation failed.";
var responses =
context.MethodInfo.GetXmlDocsElement(null)?
.Nodes()
.OfType<XElement>()
.Where(x => x.Name == "response")
.Where(x => x.Attribute("code") != null)
?? Enumerable.Empty<XElement>();
var response = new OpenApiResponse { Description = description, Schema = errorSchema };
foreach (var response in responses)
{
AddResponse(response.Attribute("code")!.Value, response.Value);
}
operation.Responses["500"] = response;
if (!string.Equals(context.OperationDescription.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
{
AddResponse("400", "Validation error.");
}
AddResponse("500", "Operation failed.");
foreach (var (code, response) in operation.Responses)
{
if (code != "404" && code.StartsWith("4", StringComparison.OrdinalIgnoreCase) && response.Schema == null)
if (response.Schema == null)
{
response.Schema = errorSchema;
if (IsErrorCode(code) && code != "404")
{
response.Schema = GetErrorSchema(context);
}
}
}
return true;
}
private static void CleanupResponses(OpenApiOperation operation)
private static bool IsErrorCode(string code)
{
foreach (var (code, response) in operation.Responses.ToList())
{
if (string.IsNullOrWhiteSpace(response.Description) ||
response.Description?.Contains("=&gt;", StringComparison.Ordinal) == true ||
response.Description?.Contains("=>", StringComparison.Ordinal) == true)
{
operation.Responses.Remove(code);
}
}
return !code.StartsWith("2", StringComparison.OrdinalIgnoreCase);
}
private static JsonSchema GetErrorSchema(DocumentProcessorContext context)
private static JsonSchema GetErrorSchema(OperationProcessorContext context)
{
var errorType = typeof(ErrorDto).ToContextualType();

30
backend/src/Squidex/Areas/Api/Config/OpenApi/FixProcessor.cs

@ -1,30 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class FixProcessor : IOperationProcessor
{
private static readonly JsonSchema StringSchema = new JsonSchema { Type = JsonObjectType.String };
public bool Process(OperationProcessorContext context)
{
foreach (var (_, parameter) in context.Parameters)
{
if (parameter.IsRequired && parameter.Schema is { Type: JsonObjectType.String })
{
parameter.Schema = StringSchema;
}
}
return true;
}
}

77
backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs

@ -12,13 +12,13 @@ using NJsonSchema.Generation.TypeMappers;
using NodaTime;
using NSwag.Generation;
using NSwag.Generation.Processors;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;
@ -27,15 +27,12 @@ public static class OpenApiServices
public static void AddSquidexOpenApiSettings(this IServiceCollection services)
{
services.AddSingletonAs<ErrorDtoProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<RuleActionProcessor>()
.As<IDocumentProcessor>();
.As<IOperationProcessor>();
services.AddSingletonAs<CommonProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<XmlTagProcessor>()
services.AddSingletonAs<TagXmlProcessor>()
.As<IDocumentProcessor>();
services.AddSingletonAs<SecurityProcessor>()
@ -44,14 +41,11 @@ public static class OpenApiServices
services.AddSingletonAs<ScopesProcessor>()
.As<IOperationProcessor>();
services.AddSingletonAs<FixProcessor>()
.As<IOperationProcessor>();
services.AddSingletonAs<TagByGroupNameProcessor>()
.As<IOperationProcessor>();
services.AddSingletonAs<XmlResponseTypesProcessor>()
.As<IOperationProcessor>();
services.AddSingletonAs<SchemaNameGenerator>()
.As<ISchemaNameGenerator>();
services.AddSingletonAs<JsonSchemaGenerator>()
.AsSelf();
@ -63,7 +57,7 @@ public static class OpenApiServices
{
var settings = new JsonSchemaGeneratorSettings();
ConfigureSchemaSettings(settings, true);
ConfigureSchemaSettings(settings, c.GetRequiredService<TypeRegistry>(), true);
return settings;
});
@ -72,7 +66,7 @@ public static class OpenApiServices
{
var settings = new OpenApiDocumentGeneratorSettings();
ConfigureSchemaSettings(settings, true);
ConfigureSchemaSettings(settings, c.GetRequiredService<TypeRegistry>(), true);
foreach (var processor in c.GetRequiredService<IEnumerable<IDocumentProcessor>>())
{
@ -82,56 +76,42 @@ public static class OpenApiServices
return settings;
});
services.AddOpenApiDocument(settings =>
services.AddOpenApiDocument((settings, services) =>
{
settings.Title = "Squidex API";
ConfigureSchemaSettings(settings);
ConfigureSchemaSettings(settings, services.GetRequiredService<TypeRegistry>(), false);
settings.OperationProcessors.Add(new QueryParamsProcessor("/api/apps/{app}/assets"));
});
}
private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, bool flatten = false)
private static void ConfigureSchemaSettings(JsonSchemaGeneratorSettings settings, TypeRegistry typeRegistry, bool flatten)
{
settings.AllowReferencesWithProperties = true;
settings.ReflectionService = new ReflectionServices();
settings.TypeMappers = new List<ITypeMapper>
{
CreateAnyMap<FilterNode<JsonValue>>(),
CreateAnyMap<JsonDocument>(),
CreateAnyMap<JsonValue>(),
CreateArrayMap<FieldNames>(JsonObjectType.String),
CreateObjectMap<AssetMetadata>(),
CreateObjectMap<JsonObject>(),
CreateStringMap<DomainId>(),
CreateStringMap<Instant>(JsonFormatStrings.DateTime),
CreateStringMap<Language>(),
CreateStringMap<LocalDate>(JsonFormatStrings.Date),
CreateStringMap<LocalDateTime>(JsonFormatStrings.DateTime),
CreateStringMap<Language>(),
CreateStringMap<NamedId<DomainId>>(),
CreateStringMap<NamedId<Guid>>(),
CreateStringMap<NamedId<string>>(),
CreateStringMap<RefToken>(),
CreateStringMap<Status>(),
CreateObjectMap<JsonObject>(),
CreateObjectMap<AssetMetadata>(),
CreateAnyMap<JsonDocument>(),
CreateAnyMap<JsonValue>(),
CreateAnyMap<FilterNode<JsonValue>>(),
new PrimitiveTypeMapper(typeof(FieldNames), schema =>
{
schema.Type = JsonObjectType.Array;
schema.Item = new JsonSchema
{
Type = JsonObjectType.String
};
}),
};
settings.SchemaType = NJsonSchema.SchemaType.OpenApi3;
settings.AllowReferencesWithProperties = true;
settings.FlattenInheritanceHierarchy = flatten;
settings.SchemaNameGenerator = new SchemaNameGenerator();
settings.SchemaProcessors.Add(new DiscriminatorProcessor(typeRegistry));
settings.SchemaType = NJsonSchema.SchemaType.OpenApi3;
settings.ReflectionService = new ReflectionServices();
}
private static ITypeMapper CreateObjectMap<T>()
@ -147,6 +127,19 @@ public static class OpenApiServices
});
}
private static ITypeMapper CreateArrayMap<T>(JsonObjectType itemType)
{
return new PrimitiveTypeMapper(typeof(T), schema =>
{
schema.Type = JsonObjectType.Array;
schema.Item = new JsonSchema
{
Type = itemType
};
});
}
private static ITypeMapper CreateStringMap<T>(string? format = null)
{
return new PrimitiveTypeMapper(typeof(T), schema =>

30
backend/src/Squidex/Areas/Api/Config/OpenApi/SchemaNameGenerator.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NJsonSchema.Generation;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class SchemaNameGenerator : DefaultSchemaNameGenerator
{
public override string Generate(Type type)
{
if (type.BaseType == typeof(RuleAction))
{
return $"{type.TypeName(false, "Action")}RuleActionDto";
}
if (type == typeof(RuleAction))
{
return $"RuleActionDto";
}
return base.Generate(type);
}
}

41
backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs

@ -15,45 +15,28 @@ namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class SecurityProcessor : SecurityDefinitionAppender
{
public SecurityProcessor(IUrlGenerator urlGenerator)
: base(Constants.SecurityDefinition, Enumerable.Empty<string>(), CreateOAuthSchema(urlGenerator))
: base(Constants.SecurityDefinition, new[] { Constants.ScopeApi }, CreateOAuthSchema(urlGenerator))
{
}
private static OpenApiSecurityScheme CreateOAuthSchema(IUrlGenerator urlGenerator)
{
var security = new OpenApiSecurityScheme
string BuildUrl(string endpoint)
{
Type = OpenApiSecuritySchemeType.OAuth2
};
var tokenUrl = urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/connect/token", false);
return urlGenerator.BuildUrl($"/{Constants.PrefixIdentityServer}/{endpoint}", false);
}
security.TokenUrl = tokenUrl;
SetupDescription(security, tokenUrl);
SetupFlow(security);
SetupScopes(security);
var security = new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.OpenIdConnect,
return security;
}
// The discovery endpoint
OpenIdConnectUrl = BuildUrl(".well-known/openid-configuration"),
private static void SetupFlow(OpenApiSecurityScheme security)
{
security.Flow = OpenApiOAuth2Flow.Application;
}
private static void SetupScopes(OpenApiSecurityScheme security)
{
security.Scopes = new Dictionary<string, string>
{
[Constants.ScopeApi] = "Read and write access to the API"
// Just described the token URL again.
Description = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", BuildUrl($"connect/token"), StringComparison.Ordinal)
};
}
private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl)
{
var securityText = Properties.Resources.OpenApiSecurity.Replace("<TOKEN_URL>", tokenUrl, StringComparison.Ordinal);
securityScheme.Description = securityText;
return security;
}
}

2
backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs → backend/src/Squidex/Areas/Api/Config/OpenApi/TagXmlProcessor.cs

@ -13,7 +13,7 @@ using NSwag.Generation.Processors.Contexts;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class XmlTagProcessor : IDocumentProcessor
public sealed class TagXmlProcessor : IDocumentProcessor
{
public void Process(DocumentProcessorContext context)
{

54
backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs

@ -1,54 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Text.RegularExpressions;
using Namotion.Reflection;
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Config.OpenApi;
public sealed class XmlResponseTypesProcessor : IOperationProcessor
{
private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3})[\\s]*=((&gt;)|>)[\\s]*(?<Description>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
public bool Process(OperationProcessorContext context)
{
var operation = context.OperationDescription.Operation;
var returnsDescription = context.MethodInfo.GetXmlDocsTag("returns");
if (!string.IsNullOrWhiteSpace(returnsDescription))
{
foreach (var match in ResponseRegex.Matches(returnsDescription).OfType<Match>())
{
var statusCode = match.Groups["Code"].Value;
if (!operation.Responses.TryGetValue(statusCode, out var response))
{
response = new OpenApiResponse();
operation.Responses[statusCode] = response;
}
var description = match.Groups["Description"].Value;
if (description.Contains("=&gt;", StringComparison.Ordinal))
{
ThrowHelper.InvalidOperationException("Description not formatted correcly.");
return default!;
}
response.Description = description;
}
}
return true;
}
}

14
backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs

@ -29,10 +29,8 @@ public sealed class AppAssetsController : ApiController
/// Get the app asset scripts.
/// </summary>
/// <param name="app">The name of the app to get the asset scripts for.</param>
/// <returns>
/// 200 => Asset scripts returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Asset scripts returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
@ -53,11 +51,9 @@ public sealed class AppAssetsController : ApiController
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param>
/// <returns>
/// 200 => Asset scripts updated.
/// 400 => Asset request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">Asset scripts updated.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]

28
backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -31,10 +31,8 @@ public sealed class AppClientsController : ApiController
/// Get app clients.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Clients returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Clients returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Gets all configured clients for the app with the specified name.
/// </remarks>
@ -60,11 +58,9 @@ public sealed class AppClientsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Client object that needs to be added to the app.</param>
/// <returns>
/// 201 => Client created.
/// 400 => Client request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Client created.</response>.
/// <response code="400">Client request not valid.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Create a new client for the app with the specified name.
/// The client secret is auto generated on the server and returned. The client does not expire, the access token is valid for 30 days.
@ -89,11 +85,9 @@ public sealed class AppClientsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param>
/// <returns>
/// 200 => Client updated.
/// 400 => Client request not valid.
/// 404 => Client or app not found.
/// </returns>
/// <response code="200">Client updated.</response>.
/// <response code="400">Client request not valid.</response>.
/// <response code="404">Client or app not found.</response>.
/// <remarks>
/// Only the display name can be changed, create a new client if necessary.
/// </remarks>
@ -116,10 +110,8 @@ public sealed class AppClientsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the client that must be deleted.</param>
/// <returns>
/// 200 => Client deleted.
/// 404 => Client or app not found.
/// </returns>
/// <response code="200">Client deleted.</response>.
/// <response code="404">Client or app not found.</response>.
/// <remarks>
/// The application that uses this client credentials cannot access the API after it has been revoked.
/// </remarks>

26
backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -38,10 +38,8 @@ public sealed class AppContributorsController : ApiController
/// Get app contributors.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Contributors returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Contributors returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]
@ -59,11 +57,9 @@ public sealed class AppContributorsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns>
/// 201 => Contributor assigned to app.
/// 400 => Contributor request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Contributor assigned to app.</response>.
/// <response code="400">Contributor request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status201Created)]
@ -82,10 +78,8 @@ public sealed class AppContributorsController : ApiController
/// Remove yourself.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Contributor removed.
/// 404 => Contributor or app not found.
/// </returns>
/// <response code="200">Contributor removed.</response>.
/// <response code="404">Contributor or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/contributors/me/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]
@ -105,10 +99,8 @@ public sealed class AppContributorsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the contributor.</param>
/// <returns>
/// 200 => Contributor removed.
/// 404 => Contributor or app not found.
/// </returns>
/// <response code="200">Contributor removed.</response>.
/// <response code="404">Contributor or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/contributors/{id}/")]
[ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)]

6
backend/src/Squidex/Areas/Api/Controllers/Apps/AppImageController.cs

@ -41,10 +41,8 @@ public sealed class AppImageController : ApiController
/// Get the app image.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App image found and content or (resized) image returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">App image found and content or (resized) image returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/image")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]

30
backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -32,10 +32,8 @@ public sealed class AppLanguagesController : ApiController
/// Get app languages.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Languages returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Languages returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]
@ -58,11 +56,9 @@ public sealed class AppLanguagesController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The language to add to the app.</param>
/// <returns>
/// 201 => Language created.
/// 400 => Language request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Language created.</response>.
/// <response code="400">Language request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguagesDto), 201)]
@ -83,11 +79,9 @@ public sealed class AppLanguagesController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="language">The language to update.</param>
/// <param name="request">The language object.</param>
/// <returns>
/// 200 => Language updated.
/// 400 => Language request not valid.
/// 404 => Language or app not found.
/// </returns>
/// <response code="200">Language updated.</response>.
/// <response code="400">Language request not valid.</response>.
/// <response code="404">Language or app not found.</response>.
[HttpPut]
[Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]
@ -107,11 +101,9 @@ public sealed class AppLanguagesController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="language">The language to delete from the app.</param>
/// <returns>
/// 200 => Language deleted.
/// 400 => Language is master language.
/// 404 => Language or app not found.
/// </returns>
/// <response code="200">Language deleted.</response>.
/// <response code="400">Language is master language.</response>.
/// <response code="404">Language or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/languages/{language}/")]
[ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)]

36
backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -35,10 +35,8 @@ public sealed class AppRolesController : ApiController
/// Get app roles.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Roles returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Roles returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]
@ -60,10 +58,8 @@ public sealed class AppRolesController : ApiController
/// Get app permissions.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App permissions returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">App permissions returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/roles/permissions")]
[ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)]
@ -86,11 +82,9 @@ public sealed class AppRolesController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Role object that needs to be added to the app.</param>
/// <returns>
/// 201 => Role created.
/// 400 => Role request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Role created.</response>.
/// <response code="400">Role request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), 201)]
@ -111,11 +105,9 @@ public sealed class AppRolesController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="roleName">The name of the role to be updated.</param>
/// <param name="request">Role to be updated for the app.</param>
/// <returns>
/// 200 => Role updated.
/// 400 => Role request not valid.
/// 404 => Role or app not found.
/// </returns>
/// <response code="200">Role updated.</response>.
/// <response code="400">Role request not valid.</response>.
/// <response code="404">Role or app not found.</response>.
[HttpPut]
[Route("apps/{app}/roles/{roleName}/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]
@ -136,11 +128,9 @@ public sealed class AppRolesController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="roleName">The name of the role.</param>
/// <returns>
/// 200 => Role deleted.
/// 400 => Role is in use by contributor or client or a default role.
/// 404 => Role or app not found.
/// </returns>
/// <response code="200">Role deleted.</response>.
/// <response code="400">Role is in use by contributor or client or a default role.</response>.
/// <response code="404">Role or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/roles/{roleName}/")]
[ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)]

14
backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs

@ -29,10 +29,8 @@ public sealed class AppSettingsController : ApiController
/// Get the app settings.
/// </summary>
/// <param name="app">The name of the app to get the settings for.</param>
/// <returns>
/// 200 => App settings returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">App settings returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
@ -53,11 +51,9 @@ public sealed class AppSettingsController : ApiController
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param>
/// <returns>
/// 200 => App updated.
/// 400 => App request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">App updated.</response>.
/// <response code="400">App request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]

28
backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs

@ -36,10 +36,8 @@ public sealed class AppWorkflowsController : ApiController
/// Get app workflow.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Workflows returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Workflows returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/workflows/")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -62,11 +60,9 @@ public sealed class AppWorkflowsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The new workflow.</param>
/// <returns>
/// 200 => Workflow created.
/// 400 => Workflow request not valid.
/// 404 => Workflow or app not found.
/// </returns>
/// <response code="200">Workflow created.</response>.
/// <response code="400">Workflow request not valid.</response>.
/// <response code="404">Workflow or app not found.</response>.
[HttpPost]
[Route("apps/{app}/workflows/")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -87,11 +83,9 @@ public sealed class AppWorkflowsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the workflow to update.</param>
/// <param name="request">The new workflow.</param>
/// <returns>
/// 200 => Workflow updated.
/// 400 => Workflow request not valid.
/// 404 => Workflow or app not found.
/// </returns>
/// <response code="200">Workflow updated.</response>.
/// <response code="400">Workflow request not valid.</response>.
/// <response code="404">Workflow or app not found.</response>.
[HttpPut]
[Route("apps/{app}/workflows/{id}")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]
@ -111,10 +105,8 @@ public sealed class AppWorkflowsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the workflow to update.</param>
/// <returns>
/// 200 => Workflow deleted.
/// 404 => Workflow or app not found.
/// </returns>
/// <response code="200">Workflow deleted.</response>.
/// <response code="404">Workflow or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/workflows/{id}")]
[ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)]

59
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -37,9 +37,7 @@ public sealed class AppsController : ApiController
/// <summary>
/// Get your apps.
/// </summary>
/// <returns>
/// 200 => Apps returned.
/// </returns>
/// <response code="200">Apps returned.</response>.
/// <remarks>
/// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow).
/// You will retrieve all apps, where you are assigned as a contributor.
@ -70,9 +68,7 @@ public sealed class AppsController : ApiController
/// Get team apps.
/// </summary>
/// <param name="team">The ID of the team.</param>
/// <returns>
/// 200 => Apps returned.
/// </returns>
/// <response code="200">Apps returned.</response>.
/// <remarks>
/// You can only retrieve the list of apps when you are authenticated as a user (OpenID implicit flow).
/// You will retrieve all apps, where you are assigned as a contributor.
@ -100,10 +96,8 @@ public sealed class AppsController : ApiController
/// Get an app by name.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Apps returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Apps returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -127,11 +121,9 @@ public sealed class AppsController : ApiController
/// Create a new app.
/// </summary>
/// <param name="request">The app object that needs to be added to Squidex.</param>
/// <returns>
/// 201 => App created.
/// 400 => App request not valid.
/// 409 => App name is already in use.
/// </returns>
/// <response code="201">App created.</response>.
/// <response code="400">App request not valid.</response>.
/// <response code="409">App name is already in use.</response>.
/// <remarks>
/// You can only create an app when you are authenticated as a user (OpenID implicit flow).
/// You will be assigned as owner of the new app automatically.
@ -153,11 +145,9 @@ public sealed class AppsController : ApiController
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param>
/// <returns>
/// 200 => App updated.
/// 400 => App request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">App updated.</response>.
/// <response code="400">App request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("apps/{app}/")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -175,11 +165,9 @@ public sealed class AppsController : ApiController
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The team information.</param>
/// <returns>
/// 200 => App transferred.
/// 400 => App request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">App transferred.</response>.
/// <response code="400">App request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("apps/{app}/team")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -197,11 +185,9 @@ public sealed class AppsController : ApiController
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 200 => App image uploaded.
/// 400 => App request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">App image uploaded.</response>.
/// <response code="400">App request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -218,10 +204,8 @@ public sealed class AppsController : ApiController
/// Remove the app image.
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <returns>
/// 200 => App image removed.
/// 404 => App not found.
/// </returns>
/// <response code="200">App image removed.</response>.
/// <response code="404">App not found.</response>.
[HttpDelete]
[Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
@ -238,12 +222,11 @@ public sealed class AppsController : ApiController
/// Delete the app.
/// </summary>
/// <param name="app">The name of the app to delete.</param>
/// <returns>
/// 204 => App deleted.
/// 404 => App not found.
/// </returns>
/// <response code="204">App deleted.</response>.
/// <response code="404">App not found.</response>.
[HttpDelete]
[Route("apps/{app}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission(PermissionIds.AppDelete)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteApp(string app)

12
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -52,10 +52,8 @@ public sealed class AssetContentController : ApiController
/// <param name="idOrSlug">The id or slug of the asset.</param>
/// <param name="request">The request parameters.</param>
/// <param name="more">Optional suffix that can be used to seo-optimize the link to the image Has not effect.</param>
/// <returns>
/// 200 => Asset found and content or (resized) image returned.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset found and content or (resized) image returned.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpGet]
[Route("assets/{app}/{idOrSlug}/{*more}")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
@ -81,10 +79,8 @@ public sealed class AssetContentController : ApiController
/// </summary>
/// <param name="id">The ID of the asset.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 200 => Asset found and content or (resized) image returned.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset found and content or (resized) image returned.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpGet]
[Route("assets/{id}/")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]

37
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -39,10 +39,8 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param>
/// <param name="scope">The scope of the query.</param>
/// <returns>
/// 200 => Asset folders returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Asset folders returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Get all asset folders for the app.
/// </remarks>
@ -72,11 +70,9 @@ public sealed class AssetFoldersController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The asset folder object that needs to be added to the App.</param>
/// <returns>
/// 201 => Asset folder created.
/// 400 => Asset folder request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Asset folder created.</response>.
/// <response code="400">Asset folder request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/assets/folders", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status201Created)]
@ -98,11 +94,9 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param>
/// <returns>
/// 200 => Asset folder updated.
/// 400 => Asset folder request not valid.
/// 404 => Asset folder or app not found.
/// </returns>
/// <response code="200">Asset folder updated.</response>.
/// <response code="400">Asset folder request not valid.</response>.
/// <response code="404">Asset folder or app not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)]
@ -124,11 +118,9 @@ public sealed class AssetFoldersController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param>
/// <returns>
/// 200 => Asset folder moved.
/// 400 => Asset folder request not valid.
/// 404 => Asset folder or app not found.
/// </returns>
/// <response code="200">Asset folder moved.</response>.
/// <response code="400">Asset folder request not valid.</response>.
/// <response code="404">Asset folder or app not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/folders/{id}/parent", Order = -1)]
[ProducesResponseType(typeof(AssetFolderDto), StatusCodes.Status200OK)]
@ -149,12 +141,11 @@ public sealed class AssetFoldersController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset folder to delete.</param>
/// <returns>
/// 204 => Asset folder deleted.
/// 404 => Asset folder or app not found.
/// </returns>
/// <response code="204">Asset folder deleted.</response>.
/// <response code="404">Asset folder or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAssetFolder(string app, DomainId id)

108
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -8,7 +8,6 @@
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Scripting;
@ -58,10 +57,8 @@ public sealed class AssetsController : ApiController
/// Get assets tags.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Assets tags returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Assets tags returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Get all tags for assets.
/// </remarks>
@ -85,10 +82,8 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="name">The tag to return.</param>
/// <param name="request">The required request object.</param>
/// <returns>
/// 200 => Asset tag renamed and new tags returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Asset tag renamed and new tags returned.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/tags/{name}")]
[ProducesResponseType(typeof(Dictionary<string, int>), StatusCodes.Status200OK)]
@ -108,10 +103,8 @@ public sealed class AssetsController : ApiController
/// <param name="parentId">The optional parent folder id.</param>
/// <param name="ids">The optional asset ids.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Assets returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Assets returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Get all assets for the app.
/// </remarks>
@ -137,10 +130,8 @@ public sealed class AssetsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Assets returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Assets returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Get all assets for the app.
/// </remarks>
@ -166,10 +157,8 @@ public sealed class AssetsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset to retrieve.</param>
/// <returns>
/// 200 => Asset found.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset found.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpGet]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -197,12 +186,10 @@ public sealed class AssetsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 201 => Asset created.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => App not found.
/// </returns>
/// <response code="201">Asset created.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="413">Asset exceeds the maximum upload size.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
/// </remarks>
@ -225,16 +212,14 @@ public sealed class AssetsController : ApiController
/// Upload a new asset using tus.io.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 201 => Asset created.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => App not found.
/// </returns>
/// <response code="201">Asset created.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="413">Asset exceeds the maximum upload size.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// Use the tus protocol to upload an asset.
/// </remarks>
[OpenApiIgnore]
[ApiExplorerSettings(IgnoreApi = true)]
[Route("apps/{app}/assets/tus/{**fileId}")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status201Created)]
[AssetRequestSizeLimit]
@ -263,11 +248,9 @@ public sealed class AssetsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The bulk update request.</param>
/// <returns>
/// 200 => Assets created, update or delete.
/// 400 => Assets request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="200">Assets created, update or delete.</response>.
/// <response code="400">Assets request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/assets/bulk")]
[ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)]
@ -291,12 +274,10 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The optional custom asset id.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 200 => Asset created or updated.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => App not found.
/// </returns>
/// <response code="200">Asset created or updated.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="413">Asset exceeds the maximum upload size.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
/// </remarks>
@ -321,12 +302,10 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 200 => Asset updated.
/// 400 => Asset request not valid.
/// 413 => Asset exceeds the maximum upload size.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset updated.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="413">Asset exceeds the maximum upload size.</response>.
/// <response code="404">Asset or app not found.</response>.
/// <remarks>
/// Use multipart request to upload an asset.
/// </remarks>
@ -351,11 +330,9 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 200 => Asset updated.
/// 400 => Asset request not valid.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset updated.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -377,11 +354,9 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 200 => Asset moved.
/// 400 => Asset request not valid.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="200">Asset moved.</response>.
/// <response code="400">Asset request not valid.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpPut]
[Route("apps/{app}/assets/{id}/parent")]
[ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
@ -403,12 +378,11 @@ public sealed class AssetsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the asset to delete.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 204 => Asset deleted.
/// 404 => Asset or app not found.
/// </returns>
/// <response code="204">Asset deleted.</response>.
/// <response code="404">Asset or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppAssetsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, DomainId id, DeleteAssetDto request)
@ -424,7 +398,7 @@ public sealed class AssetsController : ApiController
[Route("apps/{app}/assets/completion")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult GetScriptCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
@ -437,7 +411,7 @@ public sealed class AssetsController : ApiController
[Route("apps/{app}/assets/completion/trigger")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult GetScriptTriggerCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{

2
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs

@ -132,7 +132,7 @@ public sealed class AssetContentQueryDto
request.Headers.TryGetValue("Accept", out var accept);
return accept.Any(x => x.Contains(mimeType, StringComparison.OrdinalIgnoreCase)) && assetThumbnailGenerator.CanReadAndWrite(mimeType);
return accept.Any(x => x?.Contains(mimeType, StringComparison.OrdinalIgnoreCase) == true) && assetThumbnailGenerator.CanReadAndWrite(mimeType);
}
#if ENABLE_AVIF
if (Accepts("image/avif"))

12
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -37,10 +37,8 @@ public class BackupContentController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the backup.</param>
/// <returns>
/// 200 => Backup found and content returned.
/// 404 => Backup or app not found.
/// </returns>
/// <response code="200">Backup found and content returned.</response>.
/// <response code="404">Backup or app not found.</response>.
[HttpGet]
[Route("apps/{app}/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)]
@ -58,10 +56,8 @@ public class BackupContentController : ApiController
/// <param name="id">The ID of the backup.</param>
/// <param name="appId">The ID of the app.</param>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Backup found and content returned.
/// 404 => Backup or app not found.
/// </returns>
/// <response code="200">Backup found and content returned.</response>.
/// <response code="404">Backup or app not found.</response>.
[HttpGet]
[Route("apps/backups/{id}")]
[ResponseCache(Duration = 3600 * 24 * 30)]

24
backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -34,10 +34,8 @@ public class BackupsController : ApiController
/// Get all backup jobs.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Backups returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Backups returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)]
@ -56,14 +54,12 @@ public class BackupsController : ApiController
/// Start a new backup.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 204 => Backup started.
/// 400 => Backup contingent reached.
/// 404 => App not found.
/// </returns>
/// <response code="204">Backup started.</response>.
/// <response code="400">Backup contingent reached.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppBackupsCreate)]
[ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app)
@ -78,13 +74,11 @@ public class BackupsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The ID of the backup to delete.</param>
/// <returns>
/// 204 => Backup deleted.
/// 404 => Backup or app not found.
/// </returns>
/// <response code="204">Backup deleted.</response>.
/// <response code="404">Backup or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(BackupJobDto[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppBackupsDelete)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteBackup(string app, DomainId id)

9
backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -33,9 +33,7 @@ public class RestoreController : ApiController
/// <summary>
/// Get current restore status.
/// </summary>
/// <returns>
/// 200 => Status returned.
/// </returns>
/// <response code="200">Status returned.</response>.
[HttpGet]
[Route("apps/restore/")]
[ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)]
@ -58,11 +56,10 @@ public class RestoreController : ApiController
/// Restore a backup.
/// </summary>
/// <param name="request">The backup to restore.</param>
/// <returns>
/// 204 => Restore operation started.
/// </returns>
/// <response code="204">Restore operation started.</response>.
[HttpPost]
[Route("apps/restore/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission(PermissionIds.AdminRestore)]
public async Task<IActionResult> PostRestoreJob([FromBody] RestoreRequestDto request)
{

36
backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -41,10 +41,8 @@ public sealed class CommentsController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="resource">The path to the resource.</param>
/// <returns>
/// 200 => Watching users returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Watching users returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/watching/{*resource}")]
[ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)]
@ -66,10 +64,8 @@ public sealed class CommentsController : ApiController
/// <remarks>
/// When passing in a version you can retrieve all updates since then.
/// </remarks>
/// <returns>
/// 200 => Comments returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Comments returned.</response>.
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)]
@ -95,11 +91,9 @@ public sealed class CommentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The ID of the comments.</param>
/// <param name="request">The comment object that needs to created.</param>
/// <returns>
/// 201 => Comment created.
/// 400 => Comment request not valid.
/// 404 => App not found.
/// </returns>
/// <response code="201">Comment created.</response>.
/// <response code="400">Comment request not valid.</response>.
/// <response code="404">App not found.</response>.
[HttpPost]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentDto), 201)]
@ -123,13 +117,12 @@ public sealed class CommentsController : ApiController
/// <param name="commentsId">The ID of the comments.</param>
/// <param name="commentId">The ID of the comment.</param>
/// <param name="request">The comment object that needs to updated.</param>
/// <returns>
/// 204 => Comment updated.
/// 400 => Comment request not valid.
/// 404 => Comment or app not found.
/// </returns>
/// <response code="204">Comment updated.</response>.
/// <response code="400">Comment request not valid.</response>.
/// <response code="404">Comment or app not found.</response>.
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppCommentsUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, DomainId commentsId, DomainId commentId, [FromBody] UpsertCommentDto request)
@ -147,12 +140,11 @@ public sealed class CommentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="commentsId">The ID of the comments.</param>
/// <param name="commentId">The ID of the comment.</param>
/// <returns>
/// 204 => Comment deleted.
/// 404 => Comment or app not found.
/// </returns>
/// <response code="204">Comment deleted.</response>.
/// <response code="404">Comment or app not found.</response>.
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppCommentsDelete)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, DomainId commentsId, DomainId commentId)

11
backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs

@ -41,9 +41,7 @@ public sealed class UserNotificationsController : ApiController
/// <remarks>
/// When passing in a version you can retrieve all updates since then.
/// </remarks>
/// <returns>
/// 200 => All comments returned.
/// </returns>
/// <response code="200">All comments returned.</response>.
[HttpGet]
[Route("users/{userId}/notifications")]
[ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)]
@ -69,12 +67,11 @@ public sealed class UserNotificationsController : ApiController
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="commentId">The ID of the comment.</param>
/// <returns>
/// 204 => Comment deleted.
/// 404 => Comment not found.
/// </returns>
/// <response code="204">Comment deleted.</response>.
/// <response code="404">Comment not found.</response>.
[HttpDelete]
[Route("users/{userId}/notifications/{commentId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission]
public async Task<IActionResult> DeleteComment(DomainId userId, DomainId commentId)
{

130
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -40,10 +40,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Contents retunred.
/// 404 => Schema or app not found.
/// </returns>
/// <response code="200">Contents retunred.</response>.
/// <response code="404">Schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -70,10 +68,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => Schema or app not found.
/// </returns>
/// <response code="200">Contents returned.</response>.
/// <response code="404">Schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -101,10 +97,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param>
/// <param name="version">The optional version.</param>
/// <returns>
/// 200 => Content returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content returned.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -136,16 +130,15 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param>
/// <returns>
/// 204 => Content is valid.
/// 400 => Content not valid.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="204">Content is valid.</response>.
/// <response code="400">Content not valid.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/{schema}/{id}/validity")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContentValidity(string app, string schema, DomainId id)
@ -164,10 +157,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Contents returned.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -195,10 +186,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
/// 200 => Content returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content returned.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -226,10 +215,8 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content to fetch.</param>
/// <param name="version">The version fo the content to fetch.</param>
/// <returns>
/// 200 => Content version returned.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content version returned.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -261,11 +248,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 201 => Content created.
/// 400 => Content request not valid.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="201">Content created.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -289,11 +274,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="request">The import request.</param>
/// <returns>
/// 200 => Contents created.
/// 400 => Content request not valid.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <response code="200">Contents created.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content references, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -321,11 +304,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param>
/// <returns>
/// 201 => Contents created, update or delete.
/// 400 => Contents request not valid.
/// 404 => Contents references, schema or app not found.
/// </returns>
/// <response code="201">Contents created, update or delete.</response>.
/// <response code="400">Contents request not valid.</response>.
/// <response code="404">Contents references, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -353,11 +334,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to update.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 200 => Content created or updated.
/// 400 => Content request not valid.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <response code="200">Content created or updated.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content references, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -382,11 +361,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to update.</param>
/// <param name="request">The full data for the content item.</param>
/// <returns>
/// 200 => Content updated.
/// 400 => Content request not valid.
/// 404 => Content references, schema or app not found.
/// </returns>
/// <response code="200">Content updated.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content references, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -411,11 +388,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to patch.</param>
/// <param name="request">The patch for the content item.</param>
/// <returns>
/// 200 => Content patched.
/// 400 => Content request not valid.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content patched.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -440,11 +415,9 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to change.</param>
/// <param name="request">The status request.</param>
/// <returns>
/// 200 => Content status changed.
/// 400 => Content request not valid.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content status changed.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -468,11 +441,9 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to cancel.</param>
/// <returns>
/// 200 => Content status change cancelled.
/// 400 => Content request not valid.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content status change cancelled.</response>.
/// <response code="400">Content request not valid.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -496,10 +467,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to create the draft for.</param>
/// <returns>
/// 200 => Content draft created.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content draft created.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -523,10 +492,8 @@ public sealed class ContentsController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to delete the draft from.</param>
/// <returns>
/// 200 => Content draft deleted.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="200">Content draft deleted.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -551,16 +518,15 @@ public sealed class ContentsController : ApiController
/// <param name="schema">The name of the schema.</param>
/// <param name="id">The ID of the content item to delete.</param>
/// <param name="request">The request parameters.</param>
/// <returns>
/// 204 => Content deleted.
/// 400 => Content cannot be deleted.
/// 404 => Content, schema or app not found.
/// </returns>
/// <response code="204">Content deleted.</response>.
/// <response code="400">Content cannot be deleted.</response>.
/// <response code="404">Content, schema or app not found.</response>.
/// <remarks>
/// You can create an generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpDelete]
[Route("content/{app}/{schema}/{id}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsDeleteOwn)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string schema, DomainId id, DeleteContentDto request)

26
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -38,10 +38,8 @@ public sealed class ContentsSharedController : ApiController
/// GraphQL endpoint.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Contents returned or mutated.
/// 404 => App not found.
/// </returns>
/// <response code="200">Contents returned or mutated.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -61,10 +59,8 @@ public sealed class ContentsSharedController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Contents returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -90,10 +86,8 @@ public sealed class ContentsSharedController : ApiController
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <response code="200">Contents returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
@ -120,11 +114,9 @@ public sealed class ContentsSharedController : ApiController
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param>
/// <returns>
/// 201 => Contents created, update or delete.
/// 400 => Contents request not valid.
/// 404 => Contents references, schema or app not found.
/// </returns>
/// <response code="201">Contents created, update or delete.</response>.
/// <response code="400">Contents request not valid.</response>.
/// <response code="404">Contents references, schema or app not found.</response>.
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>

14
backend/src/Squidex/Areas/Api/Controllers/Diagnostics/DiagnosticsController.cs

@ -30,12 +30,11 @@ public sealed class DiagnosticsController : ApiController
/// <summary>
/// Creates a dump and writes it into storage..
/// </summary>
/// <returns>
/// 204 => Dump created successful.
/// 501 => Not configured.
/// </returns>
/// <response code="204">Dump created successful.</response>.
/// <response code="501">Not configured.</response>.
[HttpGet]
[Route("diagnostics/dump")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.Admin)]
public async Task<IActionResult> GetDump()
{
@ -52,12 +51,11 @@ public sealed class DiagnosticsController : ApiController
/// <summary>
/// Creates a gc dump and writes it into storage.
/// </summary>
/// <returns>
/// 204 => Dump created successful.
/// 501 => Not configured.
/// </returns>
/// <response code="204">Dump created successful.</response>.
/// <response code="501">Not configured.</response>.
[HttpGet]
[Route("diagnostics/gcdump")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.Admin)]
public async Task<IActionResult> GetGCDump()
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save