diff --git a/Dockerfile b/Dockerfile
index 6eefd8277..130de2b25 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,11 +20,11 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
# Test Backend
RUN dotnet restore \
- && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
+ && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \
- && dotnet test tests/Squidex.Tests/Squidex.Tests.csproj
+ && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64
diff --git a/Dockerfile.build b/Dockerfile.build
index 6f877d600..30a5d3091 100644
--- a/Dockerfile.build
+++ b/Dockerfile.build
@@ -17,11 +17,11 @@ RUN cp -a /tmp/node_modules src/Squidex/ \
# Test Backend
RUN dotnet restore \
- && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
+ && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \
- && dotnet test tests/Squidex.Tests/Squidex.Tests.csproj
+ && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release
\ No newline at end of file
diff --git a/Squidex.sln b/Squidex.sln
index 6d8cfb71d..26a5f4eeb 100644
--- a/Squidex.sln
+++ b/Squidex.sln
@@ -30,10 +30,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_00", "tools\Migrate
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Shared", "src\Squidex.Shared\Squidex.Shared.csproj", "{5E75AB7D-6F01-4313-AFF1-7F7128FFD71F}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apps", "apps", "{C9809D59-6665-471E-AD87-5AC624C65892}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "users", "users", "{C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users", "src\Squidex.Domain.Users\Squidex.Domain.Users.csproj", "{F7771E22-47BD-45C4-A133-FD7F1DE27CA0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.MongoDb", "src\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj", "{27CF800D-890F-4882-BF05-44EC3233537D}"
@@ -61,12 +57,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entitie
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{A4823E14-C0E5-4A4D-B28F-27424C25C3C7}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squidex.Tests\Squidex.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Web.Tests", "tests\Squidex.Web.Tests\Squidex.Web.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Extensions", "extensions\Squidex.Extensions\Squidex.Extensions.csproj", "{F3C41B82-6A67-409A-B7FE-54543EE4F38B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{7EDE8CF1-B1E4-4005-B154-834B944E0D7A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Web", "src\Squidex.Web\Squidex.Web.csproj", "{5B2D251F-46E3-486A-AE16-E3FE06B559ED}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -337,35 +337,47 @@ Global
{F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x64.Build.0 = Release|Any CPU
{F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.ActiveCfg = Release|Any CPU
{F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.Build.0 = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x64.Build.0 = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x86.Build.0 = Debug|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x64.ActiveCfg = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x64.Build.0 = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.ActiveCfg = Release|Any CPU
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
- {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {C9809D59-6665-471E-AD87-5AC624C65892}
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
- {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {C9809D59-6665-471E-AD87-5AC624C65892}
+ {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{945871B1-77B8-43FB-B53C-27CF385AB756} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{B51126A8-0D75-4A79-867D-10724EC6AC84} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
{5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
- {C9809D59-6665-471E-AD87-5AC624C65892} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
- {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
- {F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}
- {27CF800D-890F-4882-BF05-44EC3233537D} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}
- {42184546-E3CB-4D4F-9495-43979B9C63B9} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}
+ {F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
+ {27CF800D-890F-4882-BF05-44EC3233537D} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
+ {42184546-E3CB-4D4F-9495-43979B9C63B9} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{EF75E488-1324-4E18-A1BD-D3A05AE67B1F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
- {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892}
- {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892}
- {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892}
- {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892}
- {7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892}
+ {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {7DA5B308-D950-4496-93D5-21D6C4D91644} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
+ {7E8CC864-4C6E-496F-A672-9F9AD8874835} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{F3C41B82-6A67-409A-B7FE-54543EE4F38B} = {FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}
+ {5B2D251F-46E3-486A-AE16-E3FE06B559ED} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}
diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
index baff5dc43..eb6c041a4 100644
--- a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Algolia
{
- [RuleActionHandler(typeof(AlgoliaActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#0d9bf9",
@@ -21,14 +21,20 @@ namespace Squidex.Extensions.Actions.Algolia
{
[Required]
[Display(Name = "Application Id", Description = "The application ID.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string AppId { get; set; }
[Required]
[Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string ApiKey { get; set; }
[Required]
- [Display(Name = "Index Name", Description = "THe name of the index.")]
+ [Display(Name = "Index Name", Description = "The name of the index.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string IndexName { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs
new file mode 100644
index 000000000..c668ba003
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Algolia
+{
+ public sealed class AlgoliaPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
index 0ee5de663..002da0d83 100644
--- a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
+++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
@@ -8,12 +8,12 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.AzureQueue
{
- [RuleActionHandler(typeof(AzureQueueActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#0d9bf9",
@@ -23,11 +23,15 @@ namespace Squidex.Extensions.Actions.AzureQueue
public sealed class AzureQueueAction : RuleAction
{
[Required]
- [Display(Name = "Connection String", Description = "The connection string to the storage account.")]
+ [Display(Name = "Connection", Description = "The connection string to the storage account.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string ConnectionString { get; set; }
[Required]
[Display(Name = "Queue", Description = "The name of the queue.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string Queue { get; set; }
protected override IEnumerable CustomValidate()
diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs
new file mode 100644
index 000000000..4dce57e2b
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.AzureQueue
+{
+ public sealed class AzureQueuePlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
index 4711dec26..8e1c04011 100644
--- a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
@@ -7,12 +7,12 @@
using System;
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Discourse
{
- [RuleActionHandler(typeof(DiscourseActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#eB6121",
@@ -23,28 +23,37 @@ namespace Squidex.Extensions.Actions.Discourse
{
[AbsoluteUrl]
[Required]
- [Display(Name = "Url", Description = "he url to the discourse server.")]
+ [Display(Name = "Server Url", Description = "The url to the discourse server.")]
+ [DataType(DataType.Url)]
public Uri Url { get; set; }
[Required]
[Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")]
+ [DataType(DataType.Text)]
public string ApiKey { get; set; }
[Required]
- [Display(Name = "Api Username", Description = "The api username to authenticate to your discourse server.")]
+ [Display(Name = "Api User", Description = "The api username to authenticate to your discourse server.")]
+ [DataType(DataType.Text)]
public string ApiUsername { get; set; }
[Required]
[Display(Name = "Text", Description = "The text as markdown.")]
+ [DataType(DataType.MultilineText)]
+ [Formattable]
public string Text { get; set; }
[Display(Name = "Title", Description = "The optional title when creating new topics.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string Title { get; set; }
[Display(Name = "Topic", Description = "The optional topic id.")]
+ [DataType(DataType.Custom)]
public int? Topic { get; set; }
[Display(Name = "Category", Description = "The optional category id.")]
+ [DataType(DataType.Custom)]
public int? Category { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
index b0811ba17..d408569df 100644
--- a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
+++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
@@ -34,7 +34,6 @@ namespace Squidex.Extensions.Actions.Discourse
var json = new Dictionary
{
- ["raw"] = Format(action.Text, @event),
["title"] = Format(action.Title, @event)
};
@@ -56,6 +55,8 @@ namespace Squidex.Extensions.Actions.Discourse
RequestBody = requestBody
};
+ json["raw"] = Format(action.Text, @event);
+
var description =
action.Topic.HasValue ?
DescriptionCreateTopic :
diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs
new file mode 100644
index 000000000..e78e04d54
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Discourse
+{
+ public sealed class DiscoursePlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
index 620f9ef6e..826f86f6d 100644
--- a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
+++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
@@ -7,12 +7,12 @@
using System;
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.ElasticSearch
{
- [RuleActionHandler(typeof(ElasticSearchActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#1e5470",
@@ -23,21 +23,28 @@ namespace Squidex.Extensions.Actions.ElasticSearch
{
[AbsoluteUrl]
[Required]
- [Display(Name = "Host", Description = "The hostname of the elastic search instance or cluster.")]
+ [Display(Name = "Server Url", Description = "The url to the elastic search instance or cluster.")]
+ [DataType(DataType.Url)]
public Uri Host { get; set; }
[Required]
[Display(Name = "Index Name", Description = "The name of the index.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string IndexName { get; set; }
[Required]
[Display(Name = "Index Type", Description = "The name of the index type.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string IndexType { get; set; }
[Display(Name = "Username", Description = "The optional username.")]
+ [DataType(DataType.Text)]
public string Username { get; set; }
[Display(Name = "Password", Description = "The optional password.")]
+ [DataType(DataType.Text)]
public string Password { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
index 991725964..10c3b0a79 100644
--- a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
+++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
@@ -45,9 +45,9 @@ namespace Squidex.Extensions.Actions.ElasticSearch
var ruleJob = new ElasticSearchJob
{
Host = action.Host.ToString(),
- ContentId = contentId,
IndexName = Format(action.IndexName, @event),
- IndexType = Format(action.IndexType, @event)
+ IndexType = Format(action.IndexType, @event),
+ ContentId = contentId
};
if (contentEvent.Type == EnrichedContentEventType.Deleted ||
diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs
new file mode 100644
index 000000000..a40f610b0
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.ElasticSearch
+{
+ public sealed class ElasticSearchPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs b/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
index 0de1cb24f..e6ab737b7 100644
--- a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Email
{
- [RuleActionHandler(typeof(EmailActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#333300",
@@ -20,39 +20,53 @@ namespace Squidex.Extensions.Actions.Email
public sealed class EmailAction : RuleAction
{
[Required]
- [Display(Name = "ServerHost", Description = "The IP address or host to the SMTP server.")]
+ [Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")]
+ [DataType(DataType.Text)]
public string ServerHost { get; set; }
[Required]
- [Display(Name = "ServerPort", Description = "The port to the SMTP server.")]
+ [Display(Name = "Server Port", Description = "The port to the SMTP server.")]
+ [DataType(DataType.Custom)]
public int ServerPort { get; set; }
[Required]
- [Display(Name = "ServerUseSsl", Description = "Specify whether the SMPT client uses Secure Sockets Layer (SSL) to encrypt the connection.")]
+ [Display(Name = "Use SSL", Description = "Specify whether the SMPT client uses Secure Sockets Layer (SSL) to encrypt the connection.")]
+ [DataType(DataType.Custom)]
public bool ServerUseSsl { get; set; }
[Required]
- [Display(Name = "ServerUsername", Description = "The username for the SMTP server.")]
- public string ServerUsername { get; set; }
+ [Display(Name = "Password", Description = "The password for the SMTP server.")]
+ [DataType(DataType.Password)]
+ public string ServerPassword { get; set; }
[Required]
- [Display(Name = "ServerPassword", Description = "The password for the SMTP server.")]
- public string ServerPassword { get; set; }
+ [Display(Name = "Username", Description = "The username for the SMTP server.")]
+ [DataType(DataType.Text)]
+ [Formattable]
+ public string ServerUsername { get; set; }
[Required]
- [Display(Name = "MessageFrom", Description = "The email sending address.")]
+ [Display(Name = "From Address", Description = "The email sending address.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string MessageFrom { get; set; }
[Required]
- [Display(Name = "MessageTo", Description = "The email message will be sent to.")]
+ [Display(Name = "To Address", Description = "The email message will be sent to.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string MessageTo { get; set; }
[Required]
- [Display(Name = "MessageSubject", Description = "The subject line for this email message.")]
+ [Display(Name = "Subject", Description = "The subject line for this email message.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string MessageSubject { get; set; }
[Required]
- [Display(Name = "MessageBody", Description = "The message body.")]
+ [Display(Name = "Body", Description = "The message body.")]
+ [DataType(DataType.MultilineText)]
+ [Formattable]
public string MessageBody { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs b/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs
new file mode 100644
index 000000000..dd9140866
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Email
+{
+ public sealed class EmailPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
index 928dab189..d227e8ce1 100644
--- a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Fastly
{
- [RuleActionHandler(typeof(FastlyActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#e23335",
@@ -21,10 +21,12 @@ namespace Squidex.Extensions.Actions.Fastly
{
[Required]
[Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")]
+ [DataType(DataType.Text)]
public string ApiKey { get; set; }
[Required]
[Display(Name = "Service Id", Description = "The ID of the fastly service.")]
+ [DataType(DataType.Text)]
public string ServiceId { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs
new file mode 100644
index 000000000..d5e4f76b9
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Fastly
+{
+ public sealed class FastlyPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
index 1363aa430..3781eb9de 100644
--- a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Medium
{
- [RuleActionHandler(typeof(MediumActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#00ab6c",
@@ -21,26 +21,37 @@ namespace Squidex.Extensions.Actions.Medium
{
[Required]
[Display(Name = "Access Token", Description = "The self issued access token.")]
+ [DataType(DataType.Text)]
public string AccessToken { get; set; }
[Required]
[Display(Name = "Title", Description = "The title, used for the url.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string Title { get; set; }
[Required]
[Display(Name = "Content", Description = "The content, either html or markdown.")]
+ [DataType(DataType.MultilineText)]
+ [Formattable]
public string Content { get; set; }
[Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string CanonicalUrl { get; set; }
- [Display(Name = "PublicationId", Description = "Optional publication id.")]
- public string PublicationId { get; set; }
-
[Display(Name = "Tags", Description = "The optional comma separated list of tags.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string Tags { get; set; }
+ [Display(Name = "Publication Id", Description = "Optional publication id.")]
+ [DataType(DataType.Text)]
+ public string PublicationId { get; set; }
+
[Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")]
+ [DataType(DataType.Custom)]
public bool IsHtml { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs
new file mode 100644
index 000000000..5d8839f89
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Medium
+{
+ public sealed class MediumPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
index 47c37eafa..44285379f 100644
--- a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Prerender
{
- [RuleActionHandler(typeof(PrerenderActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#2c3e50",
@@ -21,10 +21,13 @@ namespace Squidex.Extensions.Actions.Prerender
{
[Required]
[Display(Name = "Token", Description = "The prerender token from your account.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public string Token { get; set; }
[Required]
[Display(Name = "Url", Description = "The url to recache.")]
+ [DataType(DataType.Text)]
public string Url { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs
new file mode 100644
index 000000000..608cb32d4
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Prerender
+{
+ public sealed class PrerenderPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs b/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
deleted file mode 100644
index 5da96ebf5..000000000
--- a/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using Squidex.Domain.Apps.Core.HandleRules;
-using Squidex.Infrastructure;
-
-namespace Squidex.Extensions.Actions
-{
- [AttributeUsage(AttributeTargets.Class, Inherited = false)]
- public sealed class RuleActionHandlerAttribute : Attribute
- {
- public Type HandlerType { get; }
-
- public RuleActionHandlerAttribute(Type handlerType)
- {
- Guard.NotNull(handlerType, nameof(handlerType));
-
- HandlerType = handlerType;
-
- if (!typeof(IRuleActionHandler).IsAssignableFrom(handlerType))
- {
- throw new ArgumentException($"Handler type must implement {typeof(IRuleActionHandler)}.", nameof(handlerType));
- }
- }
- }
-}
diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
deleted file mode 100644
index dac6d42dc..000000000
--- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Infrastructure;
-
-namespace Squidex.Extensions.Actions
-{
- public static class RuleElementRegistry
- {
- private const string ActionSuffix = "Action";
- private const string ActionSuffixV2 = "Action";
- private static readonly HashSet ActionHandlerTypes = new HashSet();
- private static readonly Dictionary ActionTypes = new Dictionary();
-
- public static IReadOnlyDictionary Triggers
- {
- get { return TriggerTypes.All; }
- }
-
- public static IReadOnlyDictionary Actions
- {
- get { return ActionTypes; }
- }
-
- public static IReadOnlyCollection ActionHandlers
- {
- get { return ActionHandlerTypes; }
- }
-
- static RuleElementRegistry()
- {
- var actionTypes =
- typeof(RuleElementRegistry).Assembly
- .GetTypes()
- .Where(x => typeof(RuleAction).IsAssignableFrom(x))
- .Where(x => x.GetCustomAttribute() != null)
- .Where(x => x.GetCustomAttribute() != null)
- .ToList();
-
- foreach (var actionType in actionTypes)
- {
- var name = GetActionName(actionType);
-
- var metadata = actionType.GetCustomAttribute();
-
- ActionTypes[name] =
- new RuleElement
- {
- Type = actionType,
- Display = metadata.Display,
- Description = metadata.Description,
- IconColor = metadata.IconColor,
- IconImage = metadata.IconImage,
- ReadMore = metadata.ReadMore
- };
-
- ActionHandlerTypes.Add(actionType.GetCustomAttribute().HandlerType);
- }
- }
-
- public static TypeNameRegistry MapRuleActions(this TypeNameRegistry typeNameRegistry)
- {
- foreach (var actionType in ActionTypes.Values)
- {
- typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
- }
-
- return typeNameRegistry;
- }
-
- private static string GetActionName(Type type)
- {
- return type.TypeName(false, ActionSuffix, ActionSuffixV2);
- }
- }
-}
diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
index 792a0f3fc..9b1382530 100644
--- a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
@@ -7,12 +7,12 @@
using System;
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Slack
{
- [RuleActionHandler(typeof(SlackActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#5c3a58",
@@ -24,10 +24,13 @@ namespace Squidex.Extensions.Actions.Slack
[AbsoluteUrl]
[Required]
[Display(Name = "Webhook Url", Description = "The slack webhook url.")]
+ [DataType(DataType.Text)]
public Uri WebhookUrl { get; set; }
[Required]
[Display(Name = "Text", Description = "The text that is sent as message to slack.")]
+ [DataType(DataType.MultilineText)]
+ [Formattable]
public string Text { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs
new file mode 100644
index 000000000..15549a349
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Slack
+{
+ public sealed class SlackPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/TriggerTypes.cs b/extensions/Squidex.Extensions/Actions/TriggerTypes.cs
deleted file mode 100644
index 731a280f9..000000000
--- a/extensions/Squidex.Extensions/Actions/TriggerTypes.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using System.Collections.Generic;
-using Squidex.Domain.Apps.Core.Rules.Triggers;
-using Squidex.Infrastructure;
-
-namespace Squidex.Extensions.Actions
-{
- public static class TriggerTypes
- {
- private const string TriggerSuffix = "Trigger";
- private const string TriggerSuffixV2 = "TriggerV2";
-
- public static readonly IReadOnlyDictionary All = new Dictionary
- {
- [GetTriggerName(typeof(ContentChangedTriggerV2))] = new RuleElement
- {
- IconImage = "",
- IconColor = "#3389ff",
- Display = "Content changed",
- Description = "For content changes like created, updated, published, unpublished..."
- },
- [GetTriggerName(typeof(AssetChangedTriggerV2))] = new RuleElement
- {
- IconImage = "",
- IconColor = "#3389ff",
- Display = "Asset changed",
- Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..."
- },
- [GetTriggerName(typeof(SchemaChangedTrigger))] = new RuleElement
- {
- IconImage = "",
- IconColor = "#3389ff",
- Display = "Schema changed",
- Description = "When a schema definition has been created, updated, published or deleted..."
- },
- [GetTriggerName(typeof(UsageTrigger))] = new RuleElement
- {
- IconImage = "",
- IconColor = "#3389ff",
- Display = "Usage exceeded",
- Description = "When monthly API calls exceed a specified limit for one time a month..."
- }
- };
-
- private static string GetTriggerName(Type type)
- {
- return type.TypeName(false, TriggerSuffix, TriggerSuffixV2);
- }
- }
-}
diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
index a16f98c6f..67bb45e63 100644
--- a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
@@ -6,11 +6,11 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
namespace Squidex.Extensions.Actions.Twitter
{
- [RuleActionHandler(typeof(TweetActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#1da1f2",
@@ -21,14 +21,18 @@ namespace Squidex.Extensions.Actions.Twitter
{
[Required]
[Display(Name = "Access Token", Description = " The generated access token.")]
+ [DataType(DataType.Text)]
public string AccessToken { get; set; }
[Required]
[Display(Name = "Access Secret", Description = " The generated access secret.")]
+ [DataType(DataType.Text)]
public string AccessSecret { get; set; }
[Required]
[Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")]
+ [DataType(DataType.MultilineText)]
+ [Formattable]
public string Text { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs b/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs
new file mode 100644
index 000000000..2d987b3c4
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Twitter
+{
+ public sealed class TwitterPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.Configure(
+ config.GetSection("twitter"));
+
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
index e2c260b11..4c129531e 100644
--- a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
+++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
@@ -7,12 +7,11 @@
using System;
using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Infrastructure;
namespace Squidex.Extensions.Actions.Webhook
{
- [RuleActionHandler(typeof(WebhookActionHandler))]
[RuleAction(
IconImage = "",
IconColor = "#4bb958",
@@ -21,12 +20,14 @@ namespace Squidex.Extensions.Actions.Webhook
ReadMore = "https://en.wikipedia.org/wiki/Webhook")]
public sealed class WebhookAction : RuleAction
{
- [AbsoluteUrl]
[Required]
- [Display(Name = "Url", Description = "he url to the webhook.")]
+ [Display(Name = "Url", Description = "The url to the webhook.")]
+ [DataType(DataType.Text)]
+ [Formattable]
public Uri Url { get; set; }
[Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the signature.")]
+ [DataType(DataType.Text)]
public string SharedSecret { get; set; }
}
}
diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs
new file mode 100644
index 000000000..e25857123
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Actions.Webhook
+{
+ public sealed class WebhookPlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ services.AddRuleAction();
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs
new file mode 100644
index 000000000..bf586a836
--- /dev/null
+++ b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs
@@ -0,0 +1,29 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Assets;
+using Squidex.Infrastructure.Plugins;
+
+namespace Squidex.Extensions.Samples
+{
+ public sealed class MemoryAssetStorePlugin : IPlugin
+ {
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ var storeType = config.GetValue("assetStore:type");
+
+ if (string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase))
+ {
+ services.AddSingletonAs()
+ .As();
+ }
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
index d365c5a6d..0940d8a2a 100644
--- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj
+++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
@@ -3,17 +3,13 @@
netstandard2.0
7.3
-
- full
- True
-
-
+
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
index f4049da06..6154cff13 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
@@ -12,12 +12,12 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- public static class FieldRegistry
+ public class FieldRegistry : ITypeProvider
{
private const string Suffix = "Properties";
private const string SuffixOld = "FieldProperties";
- public static TypeNameRegistry MapFields(this TypeNameRegistry typeNameRegistry)
+ public void Map(TypeNameRegistry typeNameRegistry)
{
var types = typeof(FieldRegistry).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract);
@@ -32,8 +32,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld));
}
}
-
- return typeNameRegistry;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs
index 6f4f99495..83b058a77 100644
--- a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs
@@ -7,9 +7,11 @@
using System.Reflection;
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
namespace Squidex.Domain.Apps.Core
{
- public static class SquidexCoreModel
+ public sealed class SquidexCoreModel
{
public static readonly Assembly Assembly = typeof(SquidexCoreModel).Assembly;
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs
new file mode 100644
index 000000000..1113bcfbb
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs
@@ -0,0 +1,25 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public static class DependencyInjectionExtensions
+ {
+ public static IServiceCollection AddRuleAction(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction
+ {
+ services.AddSingletonAs()
+ .As();
+
+ services.AddSingleton(new RuleActionRegistration(typeof(TAction)));
+
+ return services;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs
new file mode 100644
index 000000000..296dcb840
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs
@@ -0,0 +1,16 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class FormattableAttribute : Attribute
+ {
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs
similarity index 94%
rename from extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs
rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs
index c5fa0343f..31c1f2368 100644
--- a/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs
@@ -7,7 +7,7 @@
using System;
-namespace Squidex.Extensions.Actions
+namespace Squidex.Domain.Apps.Core.HandleRules
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class RuleActionAttribute : Attribute
diff --git a/extensions/Squidex.Extensions/Actions/RuleElement.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs
similarity index 75%
rename from extensions/Squidex.Extensions/Actions/RuleElement.cs
rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs
index 6d7244c01..97a9110d1 100644
--- a/extensions/Squidex.Extensions/Actions/RuleElement.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs
@@ -6,10 +6,11 @@
// ==========================================================================
using System;
+using System.Collections.Generic;
-namespace Squidex.Extensions.Actions
+namespace Squidex.Domain.Apps.Core.HandleRules
{
- public sealed class RuleElement
+ public sealed class RuleActionDefinition
{
public Type Type { get; set; }
@@ -22,5 +23,7 @@ namespace Squidex.Extensions.Actions
public string Display { get; set; }
public string Description { get; set; }
+
+ public List Properties { get; } = new List();
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs
new file mode 100644
index 000000000..1611e76ea
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public sealed class RuleActionProperty
+ {
+ public RuleActionPropertyEditor Editor { get; set; }
+
+ public string Name { get; set; }
+
+ public string Display { get; set; }
+
+ public string Description { get; set; }
+
+ public bool IsFormattable { get; set; }
+
+ public bool IsRequired { get; set; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs
new file mode 100644
index 000000000..469e01a35
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs
@@ -0,0 +1,20 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public enum RuleActionPropertyEditor
+ {
+ Checkbox,
+ Email,
+ Number,
+ Password,
+ Text,
+ TextArea,
+ Url
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs
new file mode 100644
index 000000000..2d0477228
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public sealed class RuleActionRegistration
+ {
+ public Type ActionType { get; }
+
+ internal RuleActionRegistration(Type actionType)
+ {
+ Guard.NotNull(actionType, nameof(actionType));
+
+ ActionType = actionType;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs
index 31228cadd..ac9789a9f 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs
@@ -7,17 +7,151 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Reflection;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure;
+#pragma warning disable RECS0033 // Convert 'if' to '||' expression
+
namespace Squidex.Domain.Apps.Core.HandleRules
{
- public static class RuleRegistry
+ public sealed class RuleRegistry : ITypeProvider
{
- public static TypeNameRegistry MapRules(this TypeNameRegistry typeNameRegistry)
+ private const string ActionSuffix = "Action";
+ private const string ActionSuffixV2 = "ActionV2";
+ private readonly Dictionary actionTypes = new Dictionary();
+
+ public IReadOnlyDictionary Actions
{
+ get { return actionTypes; }
+ }
+
+ public RuleRegistry(IEnumerable registrations = null)
+ {
+ if (registrations != null)
+ {
+ foreach (var registration in registrations)
+ {
+ Add(registration.ActionType);
+ }
+ }
+ }
+
+ public void Add() where T : RuleAction
+ {
+ Add(typeof(T));
+ }
+
+ private void Add(Type actionType)
+ {
+ var metadata = actionType.GetCustomAttribute();
+
+ if (metadata == null)
+ {
+ return;
+ }
+
+ var name = GetActionName(actionType);
+
+ var definition =
+ new RuleActionDefinition
+ {
+ Type = actionType,
+ Display = metadata.Display,
+ Description = metadata.Description,
+ IconColor = metadata.IconColor,
+ IconImage = metadata.IconImage,
+ ReadMore = metadata.ReadMore
+ };
+
+ foreach (var property in actionType.GetProperties())
+ {
+ if (property.CanRead && property.CanWrite)
+ {
+ var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name };
+
+ var display = property.GetCustomAttribute();
+
+ if (!string.IsNullOrWhiteSpace(display?.Name))
+ {
+ actionProperty.Display = display.Name;
+ }
+
+ if (!string.IsNullOrWhiteSpace(display?.Description))
+ {
+ actionProperty.Description = display.Description;
+ }
+
+ var type = property.PropertyType;
+
+ if ((property.GetCustomAttribute() != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?))
+ {
+ actionProperty.IsRequired = true;
+ }
+
+ if (property.GetCustomAttribute() != null)
+ {
+ actionProperty.IsFormattable = true;
+ }
+
+ var dataType = property.GetCustomAttribute()?.DataType;
+
+ if (type == typeof(bool) || type == typeof(bool?))
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Checkbox;
+ }
+ else if (type == typeof(int) || type == typeof(int?))
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Number;
+ }
+ else if (dataType == DataType.Url)
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Url;
+ }
+ else if (dataType == DataType.Password)
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Password;
+ }
+ else if (dataType == DataType.EmailAddress)
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Email;
+ }
+ else if (dataType == DataType.MultilineText)
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.TextArea;
+ }
+ else
+ {
+ actionProperty.Editor = RuleActionPropertyEditor.Text;
+ }
+
+ definition.Properties.Add(actionProperty);
+ }
+ }
+
+ actionTypes[name] = definition;
+ }
+
+ private static bool IsNullable(Type type)
+ {
+ return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+ }
+
+ private static string GetActionName(Type type)
+ {
+ return type.TypeName(false, ActionSuffix, ActionSuffixV2);
+ }
+
+ public void Map(TypeNameRegistry typeNameRegistry)
+ {
+ foreach (var actionType in actionTypes.Values)
+ {
+ typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
+ }
+
var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract);
var addedTypes = new HashSet();
@@ -39,8 +173,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
typeNameRegistry.Map(type, type.Name);
}
}
-
- return typeNameRegistry;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
index 52e1d0d86..333692707 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
+++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs b/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs
new file mode 100644
index 000000000..38e3c0b0c
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Reflection;
+
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
+namespace Squidex.Domain.Apps.Core
+{
+ public static class SquidexCoreOperations
+ {
+ public static readonly Assembly Assembly = typeof(SquidexCoreOperations).Assembly;
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
index 2972cd648..fff4b75e7 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
@@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
foreach (var item in array)
{
- if (item is JsonNull n)
+ if (item is JsonNull)
{
result.Add(null);
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
index 189f08052..3c51b334b 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System;
+using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State;
@@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetRepository : ISnapshotStore
{
- public async Task<(AssetState Value, long Version)> ReadAsync(Guid key)
+ async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key)
{
using (Profiler.TraceMethod())
{
@@ -35,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
- public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
+ async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
using (Profiler.TraceMethod())
{
@@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
- Task ISnapshotStore.ReadAllAsync(Func callback)
+ Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct)
{
throw new NotSupportedException();
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
index affc3bcb5..bd68f23b6 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
@@ -10,69 +10,86 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using MongoDB.Bson;
using MongoDB.Driver;
+using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
+using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
+using Squidex.Infrastructure.Reflection;
+using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal class MongoContentCollection : MongoRepositoryBase
{
private readonly IAppProvider appProvider;
- private readonly string collectionName;
+ private readonly IJsonSerializer serializer;
- protected IJsonSerializer Serializer { get; }
-
- public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider, string collectionName)
+ public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
: base(database)
{
- this.collectionName = collectionName;
-
this.appProvider = appProvider;
- Serializer = serializer;
+ this.serializer = serializer;
}
- protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default)
+ protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default)
{
- await collection.Indexes.CreateOneAsync(
- new CreateIndexModel(Index.Ascending(x => x.ReferencedIds)), cancellationToken: ct);
+ return collection.Indexes.CreateManyAsync(new[]
+ {
+ new CreateIndexModel(Index
+ .Ascending(x => x.IndexedAppId)
+ .Ascending(x => x.IsDeleted)
+ .Ascending(x => x.Status)
+ .Ascending(x => x.Id)),
+ new CreateIndexModel(Index
+ .Ascending(x => x.IndexedSchemaId)
+ .Ascending(x => x.IsDeleted)
+ .Ascending(x => x.Status)
+ .Ascending(x => x.Id)),
+ new CreateIndexModel(Index
+ .Ascending(x => x.ScheduledAt)
+ .Ascending(x => x.IsDeleted)),
+ new CreateIndexModel(Index
+ .Ascending(x => x.ReferencedIds))
+ }, ct);
}
protected override string CollectionName()
{
- return collectionName;
+ return "State_Contents";
}
- public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, Status[] status = null, bool useDraft = false)
+ public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, List ids, Status[] status, bool useDraft)
{
try
{
query = query.AdjustToModel(schema.SchemaDef, useDraft);
- var filter = query.ToFilter(schema.Id, status);
+ var filter = query.ToFilter(schema.Id, ids, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems =
Collection.Find(filter)
+ .WithoutDraft(useDraft)
.ContentTake(query)
.ContentSkip(query)
.ContentSort(query)
- .Not(x => x.DataText)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
{
- entity.ParseData(schema.SchemaDef, Serializer);
+ entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create(contentCount.Result, contentItems.Result);
@@ -90,14 +107,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
- public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status = null)
+ public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status, bool useDraft)
{
- var find =
- status != null && status.Length > 0 ?
- Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) :
- Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id));
+ var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status));
- var contentItems = await find.Not(x => x.DataText).ToListAsync();
+ var contentItems = await find.WithoutDraft(useDraft).ToListAsync();
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList();
var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x)));
@@ -110,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (schema != null)
{
- entity.ParseData(schema.SchemaDef, Serializer);
+ entity.ParseData(schema.SchemaDef, serializer);
result.Add((entity, schema));
}
@@ -119,26 +133,95 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return result;
}
- public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status = null)
+ public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status, bool useDraft)
{
- var find =
- status != null && status.Length > 0 ?
- Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) :
- Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id));
+ var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status));
- var contentItems = find.Not(x => x.DataText).ToListAsync();
+ var contentItems = find.WithoutDraft(useDraft).ToListAsync();
var contentCount = find.CountDocumentsAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
{
- entity.ParseData(schema.SchemaDef, Serializer);
+ entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create(contentCount.Result, contentItems.Result);
}
+ public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, Status[] status, bool useDraft)
+ {
+ var find = Collection.Find(FilterFactory.Build(schema.Id, id, status));
+
+ var contentEntity = await find.WithoutDraft(useDraft).FirstOrDefaultAsync();
+
+ contentEntity?.ParseData(schema.SchemaDef, serializer);
+
+ return contentEntity;
+ }
+
+ public Task QueryScheduledWithoutDataAsync(Instant now, Func callback)
+ {
+ return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true)
+ .Not(x => x.DataByIds)
+ .Not(x => x.DataDraftByIds)
+ .ForEachAsync(c =>
+ {
+ callback(c);
+ });
+ }
+
+ public async Task> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode)
+ {
+ var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);
+
+ var contentEntities =
+ await Collection.Find(filter).Only(x => x.Id)
+ .ToListAsync();
+
+ return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
+ }
+
+ public async Task> QueryIdsAsync(Guid appId)
+ {
+ var contentEntities =
+ await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id)
+ .ToListAsync();
+
+ return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
+ }
+
+ public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema)
+ {
+ var contentEntity =
+ await Collection.Find(x => x.Id == key)
+ .FirstOrDefaultAsync();
+
+ if (contentEntity != null)
+ {
+ var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
+
+ contentEntity.ParseData(schema.SchemaDef, serializer);
+
+ return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
+ }
+
+ return (null, EtagVersion.NotFound);
+ }
+
+ public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default)
+ {
+ return Collection.Find(new BsonDocument()).ForEachPipelineAsync(async contentEntity =>
+ {
+ var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
+
+ contentEntity.ParseData(schema.SchemaDef, serializer);
+
+ await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
+ }, ct);
+ }
+
public Task CleanupAsync(Guid id)
{
return Collection.UpdateManyAsync(
@@ -152,5 +235,31 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
return Collection.DeleteOneAsync(x => x.Id == id);
}
+
+ public async Task UpsertAsync(MongoContentEntity content, long oldVersion)
+ {
+ try
+ {
+ await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert);
+ }
+ catch (MongoWriteException ex)
+ {
+ if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
+ {
+ var existingVersion =
+ await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version)
+ .FirstOrDefaultAsync();
+
+ if (existingVersion != null)
+ {
+ throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex);
+ }
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs
deleted file mode 100644
index bdec55cab..000000000
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MongoDB.Driver;
-using NodaTime;
-using Squidex.Domain.Apps.Core.ConvertContent;
-using Squidex.Domain.Apps.Entities.Apps;
-using Squidex.Domain.Apps.Entities.Contents;
-using Squidex.Domain.Apps.Entities.Contents.State;
-using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
-using Squidex.Domain.Apps.Entities.Schemas;
-using Squidex.Infrastructure;
-using Squidex.Infrastructure.Json;
-using Squidex.Infrastructure.MongoDb;
-using Squidex.Infrastructure.Queries;
-using Squidex.Infrastructure.Reflection;
-using Squidex.Infrastructure.States;
-
-namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
-{
- internal sealed class MongoContentDraftCollection : MongoContentCollection
- {
- public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
- : base(database, serializer, appProvider, "State_Content_Draft")
- {
- }
-
- protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default)
- {
- await collection.Indexes.CreateManyAsync(
- new[]
- {
- new CreateIndexModel(
- Index
- .Ascending(x => x.IndexedAppId)
- .Ascending(x => x.Id)
- .Ascending(x => x.IsDeleted)),
- new CreateIndexModel(
- Index
- .Ascending(x => x.IndexedSchemaId)
- .Ascending(x => x.Id)
- .Ascending(x => x.IsDeleted)),
- new CreateIndexModel(
- Index
- .Text(x => x.DataText)
- .Ascending(x => x.IndexedSchemaId)
- .Ascending(x => x.IsDeleted)
- .Ascending(x => x.Status))
- }, ct);
-
- await base.SetupCollectionAsync(collection, ct);
- }
-
- public async Task> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode)
- {
- var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);
-
- var contentEntities =
- await Collection.Find(filter).Only(x => x.Id)
- .ToListAsync();
-
- return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
- }
-
- public async Task> QueryIdsAsync(Guid appId)
- {
- var contentEntities =
- await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id)
- .ToListAsync();
-
- return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
- }
-
- public Task QueryScheduledWithoutDataAsync(Instant now, Func callback)
- {
- return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true)
- .Not(x => x.DataByIds)
- .Not(x => x.DataDraftByIds)
- .Not(x => x.DataText)
- .ForEachAsync(c =>
- {
- callback(c);
- });
- }
-
- public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
- {
- var contentEntity =
- await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id && x.IsDeleted != true).Not(x => x.DataText)
- .FirstOrDefaultAsync();
-
- contentEntity?.ParseData(schema.SchemaDef, Serializer);
-
- return contentEntity;
- }
-
- public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema)
- {
- var contentEntity =
- await Collection.Find(x => x.Id == key).Not(x => x.DataText)
- .FirstOrDefaultAsync();
-
- if (contentEntity != null)
- {
- var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
-
- contentEntity.ParseData(schema.SchemaDef, Serializer);
-
- return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
- }
-
- return (null, EtagVersion.NotFound);
- }
-
- public async Task UpsertAsync(MongoContentEntity content, long oldVersion)
- {
- try
- {
- content.DataText = content.DataDraftByIds.ToFullText();
-
- await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert);
- }
- catch (MongoWriteException ex)
- {
- if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
- {
- var existingVersion =
- await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version)
- .FirstOrDefaultAsync();
-
- if (existingVersion != null)
- {
- throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex);
- }
- }
- else
- {
- throw;
- }
- }
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
index a2346db31..9c8f3eba7 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
@@ -69,10 +69,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonJson]
public ScheduleJob ScheduleJob { get; set; }
- [BsonIgnoreIfDefault]
- [BsonElement("dt")]
- public string DataText { get; set; }
-
[BsonRequired]
[BsonElement("ai")]
public NamedId AppId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs
deleted file mode 100644
index c210297a8..000000000
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using MongoDB.Driver;
-using Squidex.Domain.Apps.Core.ConvertContent;
-using Squidex.Domain.Apps.Entities.Apps;
-using Squidex.Domain.Apps.Entities.Contents;
-using Squidex.Domain.Apps.Entities.Schemas;
-using Squidex.Infrastructure.Json;
-using Squidex.Infrastructure.MongoDb;
-
-namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
-{
- internal sealed class MongoContentPublishedCollection : MongoContentCollection
- {
- public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
- : base(database, serializer, appProvider, "State_Content_Published")
- {
- }
-
- protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default)
- {
- await collection.Indexes.CreateManyAsync(
- new[]
- {
- new CreateIndexModel(
- Index
- .Ascending(x => x.IndexedAppId)
- .Ascending(x => x.Id)),
- new CreateIndexModel(
- Index
- .Ascending(x => x.IndexedSchemaId)
- .Ascending(x => x.Id)),
- new CreateIndexModel(
- Index
- .Text(x => x.DataText)
- .Ascending(x => x.IndexedSchemaId))
- }, ct);
-
- await base.SetupCollectionAsync(collection, ct);
- }
-
- public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
- {
- var contentEntity =
- await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id).Not(x => x.DataText)
- .FirstOrDefaultAsync();
-
- contentEntity?.ParseData(schema.SchemaDef, Serializer);
-
- return contentEntity;
- }
-
- public Task UpsertAsync(MongoContentEntity content)
- {
- content.DataText = content.DataByIds.ToFullText();
- content.DataDraftByIds = null;
- content.ScheduleJob = null;
- content.ScheduledAt = null;
-
- return Collection.ReplaceOneAsync(x => x.Id == content.Id, content, new UpdateOptions { IsUpsert = true });
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
index 8adc0a065..2a74ffe74 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
@@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
+using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
@@ -28,86 +29,90 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly IMongoDatabase database;
private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer;
- private readonly MongoContentDraftCollection contentsDraft;
- private readonly MongoContentPublishedCollection contentsPublished;
+ private readonly ITextIndexer indexer;
+ private readonly MongoContentCollection contents;
- public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer)
+ public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(serializer, nameof(serializer));
+ Guard.NotNull(indexer, nameof(ITextIndexer));
this.appProvider = appProvider;
-
+ this.database = database;
+ this.indexer = indexer;
this.serializer = serializer;
- contentsDraft = new MongoContentDraftCollection(database, serializer, appProvider);
- contentsPublished = new MongoContentPublishedCollection(database, serializer, appProvider);
-
- this.database = database;
+ contents = new MongoContentCollection(database, serializer, appProvider);
}
public Task InitializeAsync(CancellationToken ct = default)
{
- return Task.WhenAll(contentsDraft.InitializeAsync(ct), contentsPublished.InitializeAsync(ct));
+ return contents.InitializeAsync(ct);
}
public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query)
{
+ Guard.NotNull(app, nameof(app));
+ Guard.NotNull(schema, nameof(schema));
+ Guard.NotNull(status, nameof(status));
+ Guard.NotNull(query, nameof(query));
+
using (Profiler.TraceMethod("QueryAsyncByQuery"))
{
- if (RequiresPublished(status))
- {
- return await contentsPublished.QueryAsync(app, schema, query);
- }
- else
+ var useDraft = UseDraft(status);
+
+ var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, useDraft);
+
+ if (fullTextIds?.Count == 0)
{
- return await contentsDraft.QueryAsync(app, schema, query, status, true);
+ return ResultList.Create(0);
}
+
+ return await contents.QueryAsync(app, schema, query, fullTextIds, status, true);
}
}
public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids)
{
+ Guard.NotNull(app, nameof(app));
+ Guard.NotNull(schema, nameof(schema));
+ Guard.NotNull(status, nameof(status));
+ Guard.NotNull(ids, nameof(ids));
+
using (Profiler.TraceMethod("QueryAsyncByIds"))
{
- if (RequiresPublished(status))
- {
- return await contentsPublished.QueryAsync(app, schema, ids);
- }
- else
- {
- return await contentsDraft.QueryAsync(app, schema, ids, status);
- }
+ var useDraft = UseDraft(status);
+
+ return await contents.QueryAsync(app, schema, ids, status, useDraft);
}
}
public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids)
{
+ Guard.NotNull(app, nameof(app));
+ Guard.NotNull(status, nameof(status));
+ Guard.NotNull(ids, nameof(ids));
+
using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema"))
{
- if (RequiresPublished(status))
- {
- return await contentsPublished.QueryAsync(app, ids);
- }
- else
- {
- return await contentsDraft.QueryAsync(app, ids, status);
- }
+ var useDraft = UseDraft(status);
+
+ return await contents.QueryAsync(app, ids, status, useDraft);
}
}
public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id)
{
+ Guard.NotNull(app, nameof(app));
+ Guard.NotNull(schema, nameof(schema));
+ Guard.NotNull(status, nameof(status));
+
using (Profiler.TraceMethod())
{
- if (RequiresPublished(status))
- {
- return await contentsPublished.FindContentAsync(app, schema, id);
- }
- else
- {
- return await contentsDraft.FindContentAsync(app, schema, id);
- }
+ var useDraft = UseDraft(status);
+
+ return await contents.FindContentAsync(app, schema, id, status, useDraft);
}
}
@@ -115,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
using (Profiler.TraceMethod())
{
- return await contentsDraft.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode);
+ return await contents.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode);
}
}
@@ -123,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
using (Profiler.TraceMethod())
{
- return await contentsDraft.QueryIdsAsync(appId);
+ return await contents.QueryIdsAsync(appId);
}
}
@@ -131,22 +136,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
using (Profiler.TraceMethod())
{
- await contentsDraft.QueryScheduledWithoutDataAsync(now, callback);
+ await contents.QueryScheduledWithoutDataAsync(now, callback);
}
}
- public Task RemoveAsync(Guid appId)
- {
- return Task.WhenAll(
- contentsDraft.RemoveAsync(appId),
- contentsPublished.RemoveAsync(appId));
- }
-
public Task ClearAsync()
{
- return Task.WhenAll(
- contentsDraft.ClearAsync(),
- contentsPublished.ClearAsync());
+ return contents.ClearAsync();
}
public Task DeleteArchiveAsync()
@@ -154,9 +150,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return database.DropCollectionAsync("States_Contents_Archive");
}
- private static bool RequiresPublished(Status[] status)
+ private static bool UseDraft(Status[] status)
{
- return status?.Length == 1 && status[0] == Status.Published;
+ return status.Length != 1 || status[0] != Status.Published;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
index 427629d1d..bb072df70 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
@@ -33,16 +33,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected Task On(AssetDeleted @event)
{
- return Task.WhenAll(
- contentsDraft.CleanupAsync(@event.AssetId),
- contentsPublished.CleanupAsync(@event.AssetId));
+ return contents.CleanupAsync(@event.AssetId);
}
protected Task On(ContentDeleted @event)
{
- return Task.WhenAll(
- contentsDraft.CleanupAsync(@event.ContentId),
- contentsPublished.CleanupAsync(@event.ContentId));
+ return contents.CleanupAsync(@event.ContentId);
}
Task IEventConsumer.ClearAsync()
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
index 32f38e982..c45d01aa5 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
@@ -6,8 +6,8 @@
// ==========================================================================
using System;
+using System.Threading;
using System.Threading.Tasks;
-using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
@@ -19,15 +19,31 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public partial class MongoContentRepository : ISnapshotStore
{
- public async Task<(ContentState Value, long Version)> ReadAsync(Guid key)
+ async Task ISnapshotStore.RemoveAsync(Guid key)
{
using (Profiler.TraceMethod())
{
- return await contentsDraft.ReadAsync(key, GetSchemaAsync);
+ await contents.RemoveAsync(key);
}
}
- public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
+ async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct)
+ {
+ using (Profiler.TraceMethod())
+ {
+ await contents.ReadAllAsync(callback, GetSchemaAsync, ct);
+ }
+ }
+
+ async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key)
+ {
+ using (Profiler.TraceMethod())
+ {
+ return await contents.ReadAsync(key, GetSchemaAsync);
+ }
+ }
+
+ async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
{
using (Profiler.TraceMethod())
{
@@ -58,15 +74,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
Version = newVersion
});
- await contentsDraft.UpsertAsync(content, oldVersion);
+ await contents.UpsertAsync(content, oldVersion);
- if (value.Status == Status.Published && !value.IsDeleted)
+ if (value.IsDeleted)
{
- await contentsPublished.UpsertAsync(content);
+ await indexer.DeleteAsync(value.SchemaId.Id, value.Id);
}
else
{
- await contentsPublished.RemoveAsync(content.Id);
+ await indexer.IndexAsync(value.SchemaId.Id, value.Id, value.Data, value.DataDraft);
}
}
}
@@ -82,15 +98,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return schema;
}
-
- Task ISnapshotStore.RemoveAsync(Guid key)
- {
- throw new NotSupportedException();
- }
-
- Task ISnapshotStore.ReadAllAsync(Func callback)
- {
- throw new NotSupportedException();
- }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
similarity index 70%
rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
index 14195985f..61c256448 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
@@ -14,12 +14,13 @@ using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
+using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
- public static class FindExtensions
+ public static class FilterFactory
{
private static readonly FilterDefinitionBuilder Filter = Builders.Filter;
@@ -109,33 +110,65 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return cursor.Skip(query);
}
- public static FilterDefinition ToFilter(this Query query, Guid schemaId, Status[] status)
+ public static IFindFluent WithoutDraft(this IFindFluent cursor, bool useDraft)
{
- var filters = new List>
+ return !useDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
+ }
+
+ public static FilterDefinition Build(Guid schemaId, Guid id, Status[] status)
+ {
+ return CreateFilter(null, schemaId, new List { id }, status, null);
+ }
+
+ public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[] status)
+ {
+ return CreateFilter(appId, null, ids, status, null);
+ }
+
+ public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[] status)
+ {
+ return CreateFilter(null, schemaId, ids, status, null);
+ }
+
+ public static FilterDefinition ToFilter(this Query query, Guid schemaId, ICollection ids, Status[] status)
+ {
+ return CreateFilter(null, schemaId, ids, status, query);
+ }
+
+ private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection ids, Status[] status, Query query)
+ {
+ var filters = new List>();
+
+ if (appId.HasValue)
{
- Filter.Eq(x => x.IndexedSchemaId, schemaId),
- Filter.Ne(x => x.IsDeleted, true)
- };
+ filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value));
+ }
- if (status != null)
+ if (schemaId.HasValue)
{
- filters.Add(Filter.In(x => x.Status, status));
+ filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value));
}
- var filter = query.BuildFilter();
+ filters.Add(Filter.Ne(x => x.IsDeleted, true));
+ filters.Add(Filter.In(x => x.Status, status));
- if (filter.Filter != null)
+ if (ids != null && ids.Count > 0)
{
- if (filter.Last)
+ if (ids.Count > 1)
{
- filters.Add(filter.Filter);
+ filters.Add(Filter.In(x => x.Id, ids));
}
else
{
- filters.Insert(0, filter.Filter);
+ filters.Add(Filter.Eq(x => x.Id, ids.First()));
}
}
+ if (query.Filter != null)
+ {
+ filters.Add(query.Filter.BuildFilter());
+ }
+
return Filter.And(filters);
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
index 6b5e6cd77..5a39bb706 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
@@ -9,6 +9,8 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.History.Repositories;
@@ -18,9 +20,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
{
public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository
{
- public MongoHistoryEventRepository(IMongoDatabase database)
+ public MongoHistoryEventRepository(IMongoDatabase database, IOptions options)
: base(database)
{
+ if (options.Value.IsCosmosDb)
+ {
+ var classMap = BsonClassMap.RegisterClassMap();
+
+ classMap.MapProperty(x => x.Created)
+ .SetElementName("_ts");
+ classMap.AutoMap();
+ }
}
protected override string CollectionName()
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
index bae5d32f0..b8c6ea495 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
index c1ee42557..d91a5ff4f 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
@@ -369,11 +369,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner };
}
- protected override AppState OnEvent(Envelope @event)
- {
- return Snapshot.Apply(@event);
- }
-
public Task> GetStateAsync()
{
return J.AsTask(Snapshot);
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
index ffd7d333a..3d281b676 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
@@ -40,11 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public Task ReserveAppAsync(Guid appId, string name)
{
- var canReserve =
- !State.Apps.ContainsKey(name) &&
- !State.Apps.Any(x => x.Value == appId) &&
- !reservedIds.Contains(appId) &&
- !reservedNames.Contains(name);
+ var canReserve = !IsInUse(appId, name) && !IsReserved(appId, name);
if (canReserve)
{
@@ -55,6 +51,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
return Task.FromResult(canReserve);
}
+ private bool IsInUse(Guid appId, string name)
+ {
+ return State.Apps.ContainsKey(name) || State.Apps.Any(x => x.Value == appId);
+ }
+
+ private bool IsReserved(Guid appId, string name)
+ {
+ return reservedIds.Contains(appId) || reservedNames.Contains(name);
+ }
+
public Task RemoveReservationAsync(Guid appId, string name)
{
reservedIds.Remove(appId);
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
index 36d8ce960..ff4f8c293 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
@@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
IsArchived = true;
}
- public AppState Apply(Envelope @event)
+ public override AppState Apply(Envelope @event)
{
var payload = (SquidexEvent)@event.Payload;
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
index 8173494e3..7ba5ea4ab 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
@@ -165,11 +165,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
}
- protected override AssetState OnEvent(Envelope @event)
- {
- return Snapshot.Apply(@event);
- }
-
public Task> GetStateAsync(long version = EtagVersion.Any)
{
return J.AsTask(GetSnapshot(version));
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
index 6e6ca7dae..928fc292d 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
@@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
await RestoreTagsAsync(appId, reader);
- await RebuildManyAsync(assetIds, id => RebuildAsync(id, (e, s) => s.Apply(e)));
+ await RebuildManyAsync(assetIds, id => RebuildAsync(id));
}
private async Task RestoreTagsAsync(Guid appId, BackupReader reader)
@@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
try
{
- await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream);
+ await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true);
}
catch (AssetAlreadyExistsException)
{
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
index e9ebd33ec..278c4e666 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
@@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
IsDeleted = true;
}
- public AssetState Apply(Envelope @event)
+ public override AssetState Apply(Envelope @event)
{
var payload = (SquidexEvent)@event.Payload;
diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
index f1bff2c44..fe6a484ec 100644
--- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
@@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
currentTask.Token.ThrowIfCancellationRequested();
- await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, currentTask.Token);
+ await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, false, currentTask.Token);
}
job.Status = JobStatus.Completed;
diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs
index 3c2cb4354..6e711347e 100644
--- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs
+++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs
@@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Backup
@@ -39,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
}
}
- protected async Task RebuildAsync(Guid key, Func, TState, TState> func) where TState : IDomainState, new()
+ protected async Task RebuildAsync(Guid key) where TState : IDomainState, new()
{
var state = new TState
{
@@ -48,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e =>
{
- state = func(e, state);
+ state = state.Apply(e);
state.Version++;
});
diff --git a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs b/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
index c5e8184ac..b9f9eb62e 100644
--- a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
@@ -5,9 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using Squidex.Infrastructure.EventSourcing;
+
namespace Squidex.Domain.Apps.Entities.Comments.State
{
public sealed class CommentsState : DomainObjectState
{
+ public override CommentsState Apply(Envelope @event)
+ {
+ return this;
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
index 5f4bef81d..d62e9c54c 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
@@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var contentIds = contentIdsBySchemaId.Values.SelectMany(x => x);
- return RebuildManyAsync(contentIds, id => RebuildAsync(id, (e, s) => s.Apply(e)));
+ return RebuildManyAsync(contentIds, id => RebuildAsync(id));
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
index f934c0deb..5adf76236 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
@@ -296,11 +296,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
- protected override ContentState OnEvent(Envelope @event)
- {
- return Snapshot.Apply(@event);
- }
-
private async Task CreateContext(Guid appId, Guid schemaId, Guid contentId, Func message)
{
var operationContext =
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs
index d1885adcd..051f66a7a 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs
@@ -22,15 +22,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable
{
- private readonly Lazy contentRepository;
- private readonly Lazy commandBus;
+ private readonly IContentRepository contentRepository;
+ private readonly ICommandBus commandBus;
private readonly IClock clock;
private readonly ISemanticLog log;
private TaskScheduler scheduler;
public ContentSchedulerGrain(
- Lazy contentRepository,
- Lazy commandBus,
+ IContentRepository contentRepository,
+ ICommandBus commandBus,
IClock clock,
ISemanticLog log)
{
@@ -39,9 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
Guard.NotNull(clock, nameof(clock));
Guard.NotNull(log, nameof(log));
- this.contentRepository = contentRepository;
- this.commandBus = commandBus;
this.clock = clock;
+
+ this.commandBus = commandBus;
+ this.contentRepository = contentRepository;
+
this.log = log;
}
@@ -66,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var now = clock.GetCurrentInstant();
- return contentRepository.Value.QueryScheduledWithoutDataAsync(now, content =>
+ return contentRepository.QueryScheduledWithoutDataAsync(now, content =>
{
return Dispatch(async () =>
{
@@ -78,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id };
- await commandBus.Value.PublishAsync(command);
+ await commandBus.PublishAsync(command);
}
}
catch (Exception ex)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
index 56ac77ede..8f5c2f0bd 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
@@ -30,7 +30,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id);
Task QueryScheduledWithoutDataAsync(Instant now, Func callback);
-
- Task RemoveAsync(Guid appId);
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
index 56936bb0a..fd375704b 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
@@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
IsDeleted = true;
}
- public ContentState Apply(Envelope @event)
+ public override ContentState Apply(Envelope @event)
{
var payload = (SquidexEvent)@event.Payload;
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs
new file mode 100644
index 000000000..e5bcfa60c
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs
@@ -0,0 +1,105 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Orleans;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Entities.Apps;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Log;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public sealed class GrainTextIndexer : ITextIndexer
+ {
+ private readonly IGrainFactory grainFactory;
+ private readonly ISemanticLog log;
+
+ public GrainTextIndexer(IGrainFactory grainFactory, ISemanticLog log)
+ {
+ Guard.NotNull(grainFactory, nameof(grainFactory));
+ Guard.NotNull(log, nameof(log));
+
+ this.grainFactory = grainFactory;
+
+ this.log = log;
+ }
+
+ public async Task DeleteAsync(Guid schemaId, Guid id)
+ {
+ var index = grainFactory.GetGrain(schemaId);
+
+ using (Profiler.TraceMethod())
+ {
+ try
+ {
+ await index.DeleteAsync(id);
+ }
+ catch (Exception ex)
+ {
+ log.LogError(ex, w => w
+ .WriteProperty("action", "DeleteTextEntry")
+ .WriteProperty("status", "Failed"));
+ }
+ }
+ }
+
+ public async Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft)
+ {
+ var index = grainFactory.GetGrain(schemaId);
+
+ using (Profiler.TraceMethod())
+ {
+ try
+ {
+ if (data != null)
+ {
+ await index.IndexAsync(id, new IndexData { Data = data });
+ }
+
+ if (dataDraft != null)
+ {
+ await index.IndexAsync(id, new IndexData { Data = dataDraft, IsDraft = true });
+ }
+ }
+ catch (Exception ex)
+ {
+ log.LogError(ex, w => w
+ .WriteProperty("action", "UpdateTextEntry")
+ .WriteProperty("status", "Failed"));
+ }
+ }
+ }
+
+ public async Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false)
+ {
+ if (string.IsNullOrWhiteSpace(queryText))
+ {
+ return null;
+ }
+
+ var index = grainFactory.GetGrain(schemaId);
+
+ using (Profiler.TraceMethod())
+ {
+ var context = CreateContext(app, useDraft);
+
+ return await index.SearchAsync(queryText, context);
+ }
+ }
+
+ private static SearchContext CreateContext(IAppEntity app, bool useDraft)
+ {
+ var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key));
+
+ return new SearchContext { Languages = languages, IsDraft = useDraft };
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs
new file mode 100644
index 000000000..b350b8ee9
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Entities.Apps;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public interface ITextIndexer
+ {
+ Task DeleteAsync(Guid schemaId, Guid id);
+
+ Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft);
+
+ Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false);
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs
new file mode 100644
index 000000000..dd1d4c5c8
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Orleans;
+using Squidex.Infrastructure.Orleans;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public interface ITextIndexerGrain : IGrainWithGuidKey
+ {
+ Task DeleteAsync(Guid id);
+
+ Task IndexAsync(Guid id, J data);
+
+ Task> SearchAsync(string queryText, SearchContext context);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs
new file mode 100644
index 000000000..7911be4a7
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Domain.Apps.Core.Contents;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public sealed class IndexData
+ {
+ public NamedContentData Data { get; set; }
+
+ public bool IsDraft { get; set; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs
new file mode 100644
index 000000000..8bd41df9c
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs
@@ -0,0 +1,65 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Lucene.Net.Analysis;
+using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Util;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public sealed class MultiLanguageAnalyzer : AnalyzerWrapper
+ {
+ private readonly StandardAnalyzer fallbackAnalyzer;
+ private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public MultiLanguageAnalyzer(LuceneVersion version)
+ : base(PER_FIELD_REUSE_STRATEGY)
+ {
+ fallbackAnalyzer = new StandardAnalyzer(version);
+
+ foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes())
+ {
+ if (typeof(Analyzer).IsAssignableFrom(type))
+ {
+ var language = type.Namespace.Split('.').Last();
+
+ if (language.Length == 2)
+ {
+ try
+ {
+ var analyzer = Activator.CreateInstance(type, version);
+
+ analyzers[language] = (Analyzer)analyzer;
+ }
+ catch (MissingMethodException)
+ {
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ protected override Analyzer GetWrappedAnalyzer(string fieldName)
+ {
+ if (fieldName.Length > 0)
+ {
+ var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer;
+
+ return analyzer;
+ }
+ else
+ {
+ return fallbackAnalyzer;
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs
new file mode 100644
index 000000000..d847ccf13
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs
@@ -0,0 +1,94 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using Lucene.Net.Index;
+using Squidex.Infrastructure.Assets;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public static class PersistenceHelper
+ {
+ private const string ArchiveFile = "Archive.zip";
+ private const string LockFile = "write.lock";
+
+ public static async Task UploadDirectoryAsync(this IAssetStore assetStore, DirectoryInfo directory, IndexCommit commit)
+ {
+ using (var fileStream = new FileStream(
+ Path.Combine(directory.FullName, ArchiveFile),
+ FileMode.Create,
+ FileAccess.ReadWrite,
+ FileShare.None,
+ 4096,
+ FileOptions.DeleteOnClose))
+ {
+ using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, true))
+ {
+ foreach (var fileName in commit.FileNames)
+ {
+ var file = new FileInfo(Path.Combine(directory.FullName, fileName));
+
+ try
+ {
+ if (!file.Name.Equals(ArchiveFile, StringComparison.OrdinalIgnoreCase) &&
+ !file.Name.Equals(LockFile, StringComparison.OrdinalIgnoreCase))
+ {
+ zipArchive.CreateEntryFromFile(file.FullName, file.Name);
+ }
+ }
+ catch (IOException)
+ {
+ continue;
+ }
+ }
+ }
+
+ fileStream.Position = 0;
+
+ await assetStore.UploadAsync(directory.Name, 0, string.Empty, fileStream, true);
+ }
+ }
+
+ public static async Task DownloadAsync(this IAssetStore assetStore, DirectoryInfo directory)
+ {
+ if (directory.Exists)
+ {
+ directory.Delete(true);
+ }
+
+ directory.Create();
+
+ using (var fileStream = new FileStream(
+ Path.Combine(directory.FullName, ArchiveFile),
+ FileMode.Create,
+ FileAccess.ReadWrite,
+ FileShare.None,
+ 4096,
+ FileOptions.DeleteOnClose))
+ {
+ try
+ {
+ await assetStore.DownloadAsync(directory.Name, 0, string.Empty, fileStream);
+
+ fileStream.Position = 0;
+
+ using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, true))
+ {
+ zipArchive.ExtractToDirectory(directory.FullName);
+ }
+ }
+ catch (AssetNotFoundException)
+ {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs
new file mode 100644
index 000000000..01bd8f78a
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public sealed class SearchContext
+ {
+ public bool IsDraft { get; set; }
+
+ public HashSet Languages { get; set; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs
new file mode 100644
index 000000000..5e1f0e57d
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs
@@ -0,0 +1,297 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Lucene.Net.Analysis;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Queries;
+using Lucene.Net.QueryParsers.Classic;
+using Lucene.Net.Search;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using Squidex.Domain.Apps.Core;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Assets;
+using Squidex.Infrastructure.Json.Objects;
+using Squidex.Infrastructure.Orleans;
+
+namespace Squidex.Domain.Apps.Entities.Contents.Text
+{
+ public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain
+ {
+ private const LuceneVersion Version = LuceneVersion.LUCENE_48;
+ private const int MaxResults = 2000;
+ private const int MaxUpdates = 100;
+ private const string MetaId = "_id";
+ private const string MetaKey = "_key";
+ private const string MetaDraft = "_dd";
+ private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(30);
+ private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version);
+ private static readonly TermsFilter DraftFilter = new TermsFilter(new Term(MetaDraft, "1"));
+ private static readonly TermsFilter NoDraftFilter = new TermsFilter(new Term(MetaDraft, "0"));
+ private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy());
+ private readonly IAssetStore assetStore;
+ private IDisposable timer;
+ private DirectoryInfo directory;
+ private IndexWriter indexWriter;
+ private IndexReader indexReader;
+ private IndexSearcher indexSearcher;
+ private QueryParser queryParser;
+ private HashSet currentLanguages;
+ private long updates;
+
+ public TextIndexerGrain(IAssetStore assetStore)
+ {
+ Guard.NotNull(assetStore, nameof(assetStore));
+
+ this.assetStore = assetStore;
+ }
+
+ public override async Task OnDeactivateAsync()
+ {
+ await DeactivateAsync(true);
+ }
+
+ protected override async Task OnActivateAsync(Guid key)
+ {
+ directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}"));
+
+ await assetStore.DownloadAsync(directory);
+
+ var config = new IndexWriterConfig(Version, Analyzer)
+ {
+ IndexDeletionPolicy = snapshotter
+ };
+
+ indexWriter = new IndexWriter(FSDirectory.Open(directory), config);
+
+ if (indexWriter.NumDocs > 0)
+ {
+ indexReader = indexWriter.GetReader(false);
+ indexSearcher = new IndexSearcher(indexReader);
+ }
+ }
+
+ public Task DeleteAsync(Guid id)
+ {
+ indexWriter.DeleteDocuments(new Term(MetaId, id.ToString()));
+
+ return TryFlushAsync();
+ }
+
+ public Task IndexAsync(Guid id, J data)
+ {
+ var docId = id.ToString();
+ var docDraft = data.Value.IsDraft ? "1" : "0";
+ var docKey = $"{docId}_{docDraft}";
+
+ indexWriter.DeleteDocuments(new Term(MetaKey, docKey));
+
+ var languages = new Dictionary();
+
+ void AppendText(string language, string text)
+ {
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ var sb = languages.GetOrAddNew(language);
+
+ if (sb.Length > 0)
+ {
+ sb.Append(" ");
+ }
+
+ sb.Append(text);
+ }
+ }
+
+ foreach (var field in data.Value.Data)
+ {
+ foreach (var fieldValue in field.Value)
+ {
+ var appendText = new Action(text => AppendText(fieldValue.Key, text));
+
+ AppendJsonText(fieldValue.Value, appendText);
+ }
+ }
+
+ if (languages.Count > 0)
+ {
+ var document = new Document();
+
+ document.AddStringField(MetaId, docId, Field.Store.YES);
+ document.AddStringField(MetaKey, docKey, Field.Store.YES);
+ document.AddStringField(MetaDraft, docDraft, Field.Store.YES);
+
+ foreach (var field in languages)
+ {
+ var fieldName = BuildFieldName(field.Key);
+
+ document.AddTextField(fieldName, field.Value.ToString(), Field.Store.NO);
+ }
+
+ indexWriter.AddDocument(document);
+ }
+
+ return TryFlushAsync();
+ }
+
+ private static void AppendJsonText(IJsonValue value, Action appendText)
+ {
+ if (value.Type == JsonValueType.String)
+ {
+ appendText(value.ToString());
+ }
+ else if (value is JsonArray array)
+ {
+ foreach (var item in array)
+ {
+ AppendJsonText(item, appendText);
+ }
+ }
+ else if (value is JsonObject obj)
+ {
+ foreach (var item in obj.Values)
+ {
+ AppendJsonText(item, appendText);
+ }
+ }
+ }
+
+ public Task> SearchAsync(string queryText, SearchContext context)
+ {
+ var result = new HashSet();
+
+ if (!string.IsNullOrWhiteSpace(queryText))
+ {
+ var query = BuildQuery(queryText, context);
+
+ if (indexReader != null)
+ {
+ var filter = context.IsDraft ? DraftFilter : NoDraftFilter;
+
+ var hits = indexSearcher.Search(query, filter, MaxResults).ScoreDocs;
+
+ foreach (var hit in hits)
+ {
+ var document = indexReader.Document(hit.Doc);
+
+ var idField = document.GetField(MetaId)?.GetStringValue();
+
+ if (idField != null && Guid.TryParse(idField, out var guid))
+ {
+ result.Add(guid);
+ }
+ }
+ }
+ }
+
+ return Task.FromResult(result.ToList());
+ }
+
+ private Query BuildQuery(string query, SearchContext context)
+ {
+ if (queryParser == null || !currentLanguages.SetEquals(context.Languages))
+ {
+ var fields =
+ context.Languages.Select(BuildFieldName)
+ .Union(Enumerable.Repeat(BuildFieldName(InvariantPartitioning.Instance.Master.Key), 1)).ToArray();
+
+ queryParser = new MultiFieldQueryParser(Version, fields, Analyzer);
+
+ currentLanguages = context.Languages;
+ }
+
+ try
+ {
+ return queryParser.Parse(query);
+ }
+ catch (ParseException ex)
+ {
+ throw new ValidationException(ex.Message);
+ }
+ }
+
+ private async Task TryFlushAsync()
+ {
+ updates++;
+
+ if (updates >= MaxUpdates)
+ {
+ await FlushAsync();
+ }
+ else
+ {
+ timer?.Dispose();
+
+ try
+ {
+ timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay);
+ }
+ catch (InvalidOperationException)
+ {
+ return;
+ }
+ }
+ }
+
+ public async Task FlushAsync()
+ {
+ if (updates > 0 && indexWriter != null)
+ {
+ indexWriter.Commit();
+ indexWriter.Flush(true, true);
+
+ indexReader?.Dispose();
+ indexReader = indexWriter.GetReader(false);
+ indexSearcher = new IndexSearcher(indexReader);
+
+ var commit = snapshotter.Snapshot();
+ try
+ {
+ await assetStore.UploadDirectoryAsync(directory, commit);
+ }
+ finally
+ {
+ snapshotter.Release(commit);
+ }
+
+ updates = 0;
+ }
+ else
+ {
+ timer?.Dispose();
+ }
+ }
+
+ public async Task DeactivateAsync(bool deleteFolder = false)
+ {
+ await TryFlushAsync();
+
+ indexWriter?.Dispose();
+ indexWriter = null;
+
+ indexReader?.Dispose();
+ indexReader = null;
+
+ if (deleteFolder && directory.Exists)
+ {
+ directory.Delete(true);
+ }
+ }
+
+ private static string BuildFieldName(string language)
+ {
+ return language;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
index 58b6341b5..7bafa9c44 100644
--- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
+++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
@@ -10,11 +10,12 @@ using System.Runtime.Serialization;
using NodaTime;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
+using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities
{
public abstract class DomainObjectState : Cloneable,
- IDomainState,
+ IDomainState,
IEntity,
IEntityWithCreatedBy,
IEntityWithLastModifiedBy,
@@ -42,6 +43,8 @@ namespace Squidex.Domain.Apps.Entities
[DataMember]
public long Version { get; set; } = EtagVersion.Empty;
+ public abstract T Apply(Envelope @event);
+
public T Clone()
{
return Clone(x => { });
diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
index 248b09542..f6073a4f6 100644
--- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
@@ -123,11 +123,6 @@ namespace Squidex.Domain.Apps.Entities.Rules
}
}
- protected override RuleState OnEvent(Envelope @event)
- {
- return Snapshot.Apply(@event);
- }
-
public Task> GetStateAsync()
{
return J.AsTask(Snapshot);
diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
index a12f9a744..48e354328 100644
--- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
@@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
IsDeleted = true;
}
- public RuleState Apply(Envelope @event)
+ public override RuleState Apply(Envelope @event)
{
var payload = (SquidexEvent)@event.Payload;
diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
index 167971a34..5905b66fd 100644
--- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
@@ -381,11 +381,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
}
}
- protected override SchemaState OnEvent(Envelope @event)
- {
- return Snapshot.Apply(@event);
- }
-
public Task> GetStateAsync()
{
return J.AsTask(Snapshot);
diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
index 800a41687..922da11a3 100644
--- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
@@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
IsDeleted = true;
}
- public SchemaState Apply(Envelope @event)
+ public override SchemaState Apply(Envelope @event)
{
var payload = (SquidexEvent)@event.Payload;
diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
index 6adb6a190..46f190489 100644
--- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
+++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
@@ -16,11 +16,15 @@
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers
-
+
diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
index bf9c58327..6d452a60f 100644
--- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
+++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
@@ -14,7 +14,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
- public abstract class SquidexDomainObjectGrain : DomainObjectGrain where T : IDomainState, new()
+ public abstract class SquidexDomainObjectGrain : DomainObjectGrain where T : IDomainState, new()
{
protected SquidexDomainObjectGrain(IStore store, ISemanticLog log)
: base(store, log)
diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
index 425bdc4d6..56df2b4a0 100644
--- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
+++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
@@ -14,7 +14,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
- public abstract class SquidexDomainObjectGrainLogSnapshots : LogSnapshotDomainObjectGrain where T : IDomainState, new()
+ public abstract class SquidexDomainObjectGrainLogSnapshots : LogSnapshotDomainObjectGrain where T : IDomainState, new()
{
protected SquidexDomainObjectGrainLogSnapshots(IStore store, ISemanticLog log)
: base(store, log)
diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs
index 8d82fce42..4bf944913 100644
--- a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs
+++ b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs
@@ -7,6 +7,8 @@
using System.Reflection;
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
namespace Squidex.Domain.Apps.Entities
{
public static class SquidexEntities
diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs
index d49cb8921..e90815bfd 100644
--- a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs
+++ b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs
@@ -7,9 +7,11 @@
using System.Reflection;
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
namespace Squidex.Domain.Apps.Events
{
- public static class SquidexEvents
+ public sealed class SquidexEvents
{
public static readonly Assembly Assembly = typeof(SquidexEvents).Assembly;
}
diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
index 351faafbf..a314a3872 100644
--- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
+++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
@@ -14,10 +14,10 @@
-
+
-
+
diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
index 291fda481..e09a88879 100644
--- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
+++ b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
@@ -110,14 +110,14 @@ namespace Squidex.Infrastructure.Assets
}
}
- public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
- return UploadCoreAsync(GetObjectName(id, version, suffix), stream, ct);
+ return UploadCoreAsync(GetObjectName(id, version, suffix), stream, overwrite, ct);
}
public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
{
- return UploadCoreAsync(fileName, stream, ct);
+ return UploadCoreAsync(fileName, stream, false, ct);
}
public Task DeleteAsync(string id, long version, string suffix)
@@ -137,13 +137,13 @@ namespace Squidex.Infrastructure.Assets
return blob.DeleteIfExistsAsync();
}
- private async Task UploadCoreAsync(string blobName, Stream stream, CancellationToken ct = default)
+ private async Task UploadCoreAsync(string blobName, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
try
{
var tempBlob = blobContainer.GetBlockBlobReference(blobName);
- await tempBlob.UploadFromStreamAsync(stream, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct);
+ await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct);
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409)
{
diff --git a/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs b/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs
new file mode 100644
index 000000000..09698e250
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs
@@ -0,0 +1,32 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents.Client;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Squidex.Infrastructure.Diagnostics
+{
+ public sealed class CosmosDbHealthCheck : IHealthCheck
+ {
+ private readonly DocumentClient documentClient;
+
+ public CosmosDbHealthCheck(Uri uri, string masterKey)
+ {
+ documentClient = new DocumentClient(uri, masterKey);
+ }
+
+ public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ await documentClient.ReadDatabaseFeedAsync();
+
+ return HealthCheckResult.Healthy("Application must query data from CosmosDB.");
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs
new file mode 100644
index 000000000..120f38704
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs
@@ -0,0 +1,16 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal static class Constants
+ {
+ public const string Collection = "Events";
+
+ public const string LeaseCollection = "Leases";
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs
new file mode 100644
index 000000000..c12c6548f
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs
@@ -0,0 +1,33 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Newtonsoft.Json;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal sealed class CosmosDbEvent
+ {
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ [JsonProperty("payload")]
+ public string Payload { get; set; }
+
+ [JsonProperty("header")]
+ public EnvelopeHeaders Headers { get; set; }
+
+ public static CosmosDbEvent FromEventData(EventData data)
+ {
+ return new CosmosDbEvent { Type = data.Type, Headers = data.Headers, Payload = data.Payload };
+ }
+
+ public EventData ToEventData()
+ {
+ return new EventData(Type, Headers, Payload);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs
new file mode 100644
index 000000000..6a5dca9b3
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs
@@ -0,0 +1,33 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Newtonsoft.Json;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal sealed class CosmosDbEventCommit
+ {
+ [JsonProperty("id")]
+ public Guid Id { get; set; }
+
+ [JsonProperty("events")]
+ public CosmosDbEvent[] Events { get; set; }
+
+ [JsonProperty("eventStreamOffset")]
+ public long EventStreamOffset { get; set; }
+
+ [JsonProperty("eventsCount")]
+ public long EventsCount { get; set; }
+
+ [JsonProperty("eventStream")]
+ public string EventStream { get; set; }
+
+ [JsonProperty("timestamp")]
+ public long Timestamp { get; set; }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs
new file mode 100644
index 000000000..a07bf13ec
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs
@@ -0,0 +1,124 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.ObjectModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents;
+using Microsoft.Azure.Documents.Client;
+using Newtonsoft.Json;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable
+ {
+ private readonly DocumentClient documentClient;
+ private readonly Uri collectionUri;
+ private readonly Uri databaseUri;
+ private readonly string masterKey;
+ private readonly string databaseId;
+ private readonly JsonSerializerSettings serializerSettings;
+
+ public JsonSerializerSettings SerializerSettings
+ {
+ get { return serializerSettings; }
+ }
+
+ public string DatabaseId
+ {
+ get { return databaseId; }
+ }
+
+ public string MasterKey
+ {
+ get { return masterKey; }
+ }
+
+ public Uri ServiceUri
+ {
+ get { return documentClient.ServiceEndpoint; }
+ }
+
+ public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings)
+ {
+ Guard.NotNull(documentClient, nameof(documentClient));
+ Guard.NotNull(serializerSettings, nameof(serializerSettings));
+ Guard.NotNullOrEmpty(masterKey, nameof(masterKey));
+ Guard.NotNullOrEmpty(database, nameof(database));
+
+ this.documentClient = documentClient;
+
+ databaseUri = UriFactory.CreateDatabaseUri(database);
+ databaseId = database;
+
+ collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection);
+
+ this.masterKey = masterKey;
+
+ this.serializerSettings = serializerSettings;
+ }
+
+ protected override void DisposeObject(bool disposing)
+ {
+ if (disposing)
+ {
+ documentClient.Dispose();
+ }
+ }
+
+ public async Task InitializeAsync(CancellationToken ct = default)
+ {
+ await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId });
+
+ await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri,
+ new DocumentCollection
+ {
+ Id = Constants.LeaseCollection,
+ });
+
+ await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri,
+ new DocumentCollection
+ {
+ IndexingPolicy = new IndexingPolicy
+ {
+ IncludedPaths = new Collection
+ {
+ new IncludedPath
+ {
+ Path = "/*",
+ Indexes = new Collection
+ {
+ Index.Range(DataType.Number),
+ Index.Range(DataType.String),
+ }
+ }
+ }
+ },
+ UniqueKeyPolicy = new UniqueKeyPolicy
+ {
+ UniqueKeys = new Collection
+ {
+ new UniqueKey
+ {
+ Paths = new Collection
+ {
+ $"/eventStream",
+ $"/eventStreamOffset"
+ }
+ }
+ }
+ },
+ Id = Constants.Collection,
+ },
+ new RequestOptions
+ {
+ PartitionKey = new PartitionKey($"/eventStream")
+ });
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
new file mode 100644
index 000000000..e0cccf559
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
@@ -0,0 +1,142 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents;
+using Squidex.Infrastructure.Log;
+using Squidex.Infrastructure.Tasks;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ public delegate bool EventPredicate(EventData data);
+
+ public partial class CosmosDbEventStore : IEventStore, IInitializable
+ {
+ public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null)
+ {
+ Guard.NotNull(subscriber, nameof(subscriber));
+
+ ThrowIfDisposed();
+
+ return new CosmosDbSubscription(this, subscriber, streamFilter, position);
+ }
+
+ public Task CreateIndexAsync(string property)
+ {
+ Guard.NotNullOrEmpty(property, nameof(property));
+
+ ThrowIfDisposed();
+
+ return Task.CompletedTask;
+ }
+
+ public async Task> QueryAsync(string streamName, long streamPosition = 0)
+ {
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+
+ ThrowIfDisposed();
+
+ using (Profiler.TraceMethod())
+ {
+ var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize);
+
+ var result = new List();
+
+ await documentClient.QueryAsync(collectionUri, query, commit =>
+ {
+ var eventStreamOffset = (int)commit.EventStreamOffset;
+
+ var commitTimestamp = commit.Timestamp;
+ var commitOffset = 0;
+
+ foreach (var @event in commit.Events)
+ {
+ eventStreamOffset++;
+
+ if (eventStreamOffset >= streamPosition)
+ {
+ var eventData = @event.ToEventData();
+ var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+
+ result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData));
+ }
+ }
+
+ return TaskHelper.Done;
+ });
+
+ return result;
+ }
+ }
+
+ public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default)
+ {
+ Guard.NotNull(callback, nameof(callback));
+ Guard.NotNullOrEmpty(property, nameof(property));
+ Guard.NotNull(value, nameof(value));
+
+ ThrowIfDisposed();
+
+ StreamPosition lastPosition = position;
+
+ var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition);
+ var filterExpression = FilterBuilder.CreateExpression(property, value);
+
+ return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct);
+ }
+
+ public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default)
+ {
+ Guard.NotNull(callback, nameof(callback));
+
+ ThrowIfDisposed();
+
+ StreamPosition lastPosition = position;
+
+ var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition);
+ var filterExpression = FilterBuilder.CreateExpression(null, null);
+
+ return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct);
+ }
+
+ private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default)
+ {
+ using (Profiler.TraceMethod())
+ {
+ await documentClient.QueryAsync(collectionUri, query, async commit =>
+ {
+ var eventStreamOffset = (int)commit.EventStreamOffset;
+
+ var commitTimestamp = commit.Timestamp;
+ var commitOffset = 0;
+
+ foreach (var @event in commit.Events)
+ {
+ eventStreamOffset++;
+
+ if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp)
+ {
+ var eventData = @event.ToEventData();
+
+ if (filterExpression(eventData))
+ {
+ var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+
+ await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData));
+ }
+ }
+
+ commitOffset++;
+ }
+ }, ct);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
new file mode 100644
index 000000000..45144e56e
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
@@ -0,0 +1,149 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents;
+using Microsoft.Azure.Documents.Client;
+using NodaTime;
+using Squidex.Infrastructure.Log;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ public partial class CosmosDbEventStore
+ {
+ private const int MaxWriteAttempts = 20;
+ private const int MaxCommitSize = 10;
+
+ public Task DeleteStreamAsync(string streamName)
+ {
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+
+ ThrowIfDisposed();
+
+ var query = FilterBuilder.AllIds(streamName);
+
+ return documentClient.QueryAsync(collectionUri, query, commit =>
+ {
+ var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString());
+
+ return documentClient.DeleteDocumentAsync(documentUri);
+ });
+ }
+
+ public Task AppendAsync(Guid commitId, string streamName, ICollection events)
+ {
+ return AppendAsync(commitId, streamName, EtagVersion.Any, events);
+ }
+
+ public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events)
+ {
+ Guard.NotEmpty(commitId, nameof(commitId));
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+ Guard.NotNull(events, nameof(events));
+ Guard.LessThan(events.Count, MaxCommitSize, "events.Count");
+
+ ThrowIfDisposed();
+
+ using (Profiler.TraceMethod())
+ {
+ if (events.Count == 0)
+ {
+ return;
+ }
+
+ var currentVersion = await GetEventStreamOffsetAsync(streamName);
+
+ if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion)
+ {
+ throw new WrongEventVersionException(currentVersion, expectedVersion);
+ }
+
+ var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events);
+
+ for (var attempt = 0; attempt < MaxWriteAttempts; attempt++)
+ {
+ try
+ {
+ await documentClient.CreateDocumentAsync(collectionUri, commit);
+
+ return;
+ }
+ catch (DocumentClientException ex)
+ {
+ if (ex.StatusCode == HttpStatusCode.Conflict)
+ {
+ currentVersion = await GetEventStreamOffsetAsync(streamName);
+
+ if (expectedVersion != EtagVersion.Any)
+ {
+ throw new WrongEventVersionException(currentVersion, expectedVersion);
+ }
+
+ if (attempt < MaxWriteAttempts)
+ {
+ expectedVersion = currentVersion;
+ }
+ else
+ {
+ throw new TimeoutException("Could not acquire a free slot for the commit within the provided time.");
+ }
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+ }
+ }
+
+ private async Task GetEventStreamOffsetAsync(string streamName)
+ {
+ var query =
+ documentClient.CreateDocumentQuery(collectionUri,
+ FilterBuilder.LastPosition(streamName));
+
+ var document = await query.FirstOrDefaultAsync();
+
+ if (document != null)
+ {
+ return document.EventStreamOffset + document.EventsCount;
+ }
+
+ return EtagVersion.Empty;
+ }
+
+ private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events)
+ {
+ var commitEvents = new CosmosDbEvent[events.Count];
+
+ var i = 0;
+
+ foreach (var e in events)
+ {
+ var mongoEvent = CosmosDbEvent.FromEventData(e);
+
+ commitEvents[i++] = mongoEvent;
+ }
+
+ var mongoCommit = new CosmosDbEventCommit
+ {
+ Id = commitId,
+ Events = commitEvents,
+ EventsCount = events.Count,
+ EventStream = streamName,
+ EventStreamOffset = expectedVersion,
+ Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks()
+ };
+
+ return mongoCommit;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs
new file mode 100644
index 000000000..fa5d8af86
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs
@@ -0,0 +1,150 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents;
+using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing;
+using Newtonsoft.Json;
+using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder;
+using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo;
+using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions;
+
+#pragma warning disable IDE0017 // Simplify object initialization
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver
+ {
+ private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource();
+ private readonly Task processorTask;
+ private readonly CosmosDbEventStore store;
+ private readonly Regex regex;
+ private readonly string hostName;
+ private readonly IEventSubscriber subscriber;
+
+ public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string streamFilter, string position = null)
+ {
+ this.store = store;
+
+ var fromBeginning = string.IsNullOrWhiteSpace(position);
+
+ if (fromBeginning)
+ {
+ hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}";
+ }
+ else
+ {
+ hostName = position;
+ }
+
+ if (!StreamFilter.IsAll(streamFilter))
+ {
+ regex = new Regex(streamFilter);
+ }
+
+ this.subscriber = subscriber;
+
+ processorTask = Task.Run(async () =>
+ {
+ try
+ {
+ Collection CreateCollection(string name)
+ {
+ var collection = new Collection();
+
+ collection.CollectionName = name;
+ collection.DatabaseName = store.DatabaseId;
+ collection.MasterKey = store.MasterKey;
+ collection.Uri = store.ServiceUri;
+
+ return collection;
+ }
+
+ var processor =
+ await new Builder()
+ .WithFeedCollection(CreateCollection(Constants.Collection))
+ .WithLeaseCollection(CreateCollection(Constants.LeaseCollection))
+ .WithHostName(hostName)
+ .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName })
+ .WithObserverFactory(this)
+ .BuildAsync();
+
+ await processor.StartAsync();
+ await processorStopRequested.Task;
+ await processor.StopAsync();
+ }
+ catch (Exception ex)
+ {
+ await subscriber.OnErrorAsync(this, ex);
+ }
+ });
+ }
+
+ public IChangeFeedObserver CreateObserver()
+ {
+ return this;
+ }
+
+ public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason)
+ {
+ if (reason == ChangeFeedObserverCloseReason.ObserverError)
+ {
+ await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed."));
+ }
+ }
+
+ public Task OpenAsync(IChangeFeedObserverContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken)
+ {
+ if (!processorStopRequested.Task.IsCompleted)
+ {
+ foreach (var document in docs)
+ {
+ if (!processorStopRequested.Task.IsCompleted)
+ {
+ var streamName = document.GetPropertyValue("eventStream");
+
+ if (regex == null || regex.IsMatch(streamName))
+ {
+ var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings);
+
+ var eventStreamOffset = (int)commit.EventStreamOffset;
+
+ foreach (var @event in commit.Events)
+ {
+ eventStreamOffset++;
+
+ var eventData = @event.ToEventData();
+
+ await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName, eventStreamOffset, eventData));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void WakeUp()
+ {
+ }
+
+ public Task StopAsync()
+ {
+ processorStopRequested.SetResult(true);
+
+ return processorTask;
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
new file mode 100644
index 000000000..b6bd7686c
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
@@ -0,0 +1,156 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+using Microsoft.Azure.Documents;
+using Squidex.Infrastructure.Json.Objects;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal static class FilterBuilder
+ {
+ public static SqlQuerySpec AllIds(string streamName)
+ {
+ var query =
+ $"SELECT TOP 1 " +
+ $" e.id," +
+ $" e.eventsCount " +
+ $"FROM {Constants.Collection} e " +
+ $"WHERE " +
+ $" e.eventStream = @name " +
+ $"ORDER BY e.eventStreamOffset DESC";
+
+ var parameters = new SqlParameterCollection
+ {
+ new SqlParameter("@name", streamName)
+ };
+
+ return new SqlQuerySpec(query, parameters);
+ }
+
+ public static SqlQuerySpec LastPosition(string streamName)
+ {
+ var query =
+ $"SELECT TOP 1 " +
+ $" e.eventStreamOffset," +
+ $" e.eventsCount " +
+ $"FROM {Constants.Collection} e " +
+ $"WHERE " +
+ $" e.eventStream = @name " +
+ $"ORDER BY e.eventStreamOffset DESC";
+
+ var parameters = new SqlParameterCollection
+ {
+ new SqlParameter("@name", streamName)
+ };
+
+ return new SqlQuerySpec(query, parameters);
+ }
+
+ public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0)
+ {
+ var query =
+ $"SELECT * " +
+ $"FROM {Constants.Collection} e " +
+ $"WHERE " +
+ $" e.eventStream = @name " +
+ $"AND e.eventStreamOffset >= @position " +
+ $"ORDER BY e.eventStreamOffset ASC";
+
+ var parameters = new SqlParameterCollection
+ {
+ new SqlParameter("@name", streamName),
+ new SqlParameter("@position", streamPosition)
+ };
+
+ return new SqlQuerySpec(query, parameters);
+ }
+
+ public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition)
+ {
+ var filters = new List();
+
+ var parameters = new SqlParameterCollection();
+
+ filters.ForPosition(parameters, streamPosition);
+ filters.ForProperty(parameters, property, value);
+
+ return BuildQuery(filters, parameters);
+ }
+
+ public static SqlQuerySpec CreateByFilter(string streamFilter, StreamPosition streamPosition)
+ {
+ var filters = new List();
+
+ var parameters = new SqlParameterCollection();
+
+ filters.ForPosition(parameters, streamPosition);
+ filters.ForRegex(parameters, streamFilter);
+
+ return BuildQuery(filters, parameters);
+ }
+
+ private static SqlQuerySpec BuildQuery(List filters, SqlParameterCollection parameters)
+ {
+ var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp";
+
+ return new SqlQuerySpec(query, parameters);
+ }
+
+ private static void ForProperty(this List filters, SqlParameterCollection parameters, string property, object value)
+ {
+ filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)");
+
+ parameters.Add(new SqlParameter("@value", value));
+ }
+
+ private static void ForRegex(this List filters, SqlParameterCollection parameters, string streamFilter)
+ {
+ if (!StreamFilter.IsAll(streamFilter))
+ {
+ if (streamFilter.Contains("^"))
+ {
+ filters.Add($"STARTSWITH(e.eventStream, @filter)");
+ }
+ else
+ {
+ filters.Add($"e.eventStream = @filter");
+ }
+
+ parameters.Add(new SqlParameter("@filter", streamFilter));
+ }
+ }
+
+ private static void ForPosition(this List filters, SqlParameterCollection parameters, StreamPosition streamPosition)
+ {
+ if (streamPosition.IsEndOfCommit)
+ {
+ filters.Add($"e.timestamp > @time");
+ }
+ else
+ {
+ filters.Add($"e.timestamp >= @time");
+ }
+
+ parameters.Add(new SqlParameter("@time", streamPosition.Timestamp));
+ }
+
+ public static EventPredicate CreateExpression(string property, object value)
+ {
+ if (!string.IsNullOrWhiteSpace(property))
+ {
+ var jsonValue = JsonValue.Create(value);
+
+ return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue);
+ }
+ else
+ {
+ return x => true;
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs
new file mode 100644
index 000000000..c24e93ff1
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs
@@ -0,0 +1,62 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Documents;
+using Microsoft.Azure.Documents.Client;
+using Microsoft.Azure.Documents.Linq;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal static class FilterExtensions
+ {
+ public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default)
+ {
+ var documentQuery = queryable.AsDocumentQuery();
+
+ using (documentQuery)
+ {
+ if (documentQuery.HasMoreResults)
+ {
+ var results = await documentQuery.ExecuteNextAsync(ct);
+
+ return results.FirstOrDefault();
+ }
+ }
+
+ return default;
+ }
+
+ public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default)
+ {
+ var query = documentClient.CreateDocumentQuery(collectionUri, querySpec);
+
+ return query.QueryAsync(handler, ct);
+ }
+
+ public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default)
+ {
+ var documentQuery = queryable.AsDocumentQuery();
+
+ using (documentQuery)
+ {
+ while (documentQuery.HasMoreResults && !ct.IsCancellationRequested)
+ {
+ var items = await documentQuery.ExecuteNextAsync(ct);
+
+ foreach (var item in items)
+ {
+ await handler(item);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs
new file mode 100644
index 000000000..f0626ee5d
--- /dev/null
+++ b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs
@@ -0,0 +1,55 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ internal sealed class StreamPosition
+ {
+ public long Timestamp { get; }
+
+ public long CommitOffset { get; }
+
+ public long CommitSize { get; }
+
+ public bool IsEndOfCommit
+ {
+ get { return CommitOffset == CommitSize - 1; }
+ }
+
+ public StreamPosition(long timestamp, long commitOffset, long commitSize)
+ {
+ Timestamp = timestamp;
+
+ CommitOffset = commitOffset;
+ CommitSize = commitSize;
+ }
+
+ public static implicit operator string(StreamPosition position)
+ {
+ var parts = new object[]
+ {
+ position.Timestamp,
+ position.CommitOffset,
+ position.CommitSize
+ };
+
+ return string.Join("-", parts);
+ }
+
+ public static implicit operator StreamPosition(string position)
+ {
+ if (!string.IsNullOrWhiteSpace(position))
+ {
+ var parts = position.Split('-');
+
+ return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2]));
+ }
+
+ return new StreamPosition(0, -1, -1);
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
index 755262980..5351ea162 100644
--- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
+++ b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
@@ -5,6 +5,8 @@
7.3
+
+
diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
index 8a2dd111f..cc05285d8 100644
--- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
+++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
@@ -6,6 +6,8 @@
// ==========================================================================
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
using EventStore.ClientAPI;
using Squidex.Infrastructure.Json;
@@ -15,24 +17,54 @@ namespace Squidex.Infrastructure.EventSourcing
{
public static class Formatter
{
- public static StoredEvent Read(ResolvedEvent resolvedEvent, IJsonSerializer serializer)
+ private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" };
+
+ public static StoredEvent Read(ResolvedEvent resolvedEvent, string prefix, IJsonSerializer serializer)
{
var @event = resolvedEvent.Event;
- var metadata = Encoding.UTF8.GetString(@event.Data);
+ var eventPayload = Encoding.UTF8.GetString(@event.Data);
+ var eventHeaders = GetHeaders(serializer, @event);
- var headersJson = Encoding.UTF8.GetString(@event.Metadata);
- var headers = serializer.Deserialize(headersJson);
+ var eventData = new EventData(@event.EventType, eventHeaders, eventPayload);
- var eventData = new EventData(@event.EventType, headers, metadata);
+ var streamName = GetStreamName(prefix, @event);
return new StoredEvent(
- @event.EventStreamId,
+ streamName,
resolvedEvent.OriginalEventNumber.ToString(),
resolvedEvent.Event.EventNumber,
eventData);
}
+ private static string GetStreamName(string prefix, RecordedEvent @event)
+ {
+ var streamName = @event.EventStreamId;
+
+ if (streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ streamName = streamName.Substring(prefix.Length + 1);
+ }
+
+ return streamName;
+ }
+
+ private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event)
+ {
+ var headersJson = Encoding.UTF8.GetString(@event.Metadata);
+ var headers = serializer.Deserialize(headersJson);
+
+ foreach (var key in headers.Keys.ToList())
+ {
+ if (PrivateHeaders.Contains(key))
+ {
+ headers.Remove(key);
+ }
+ }
+
+ return headers;
+ }
+
public static EventStoreData Write(EventData eventData, IJsonSerializer serializer)
{
var payload = Encoding.UTF8.GetBytes(eventData.Payload);
diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
index 83f81fb92..d06c1ea15 100644
--- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
+++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
+using EventStore.ClientAPI.Exceptions;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log;
@@ -52,18 +53,26 @@ namespace Squidex.Infrastructure.EventSourcing
await projectionClient.ConnectAsync(ct);
}
- public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
+ public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null)
{
- return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, streamFilter);
+ Guard.NotNull(streamFilter, nameof(streamFilter));
+
+ return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter);
}
public Task CreateIndexAsync(string property)
{
+ Guard.NotNullOrEmpty(property, nameof(property));
+
return projectionClient.CreateProjectionAsync(property, string.Empty);
}
public async Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default)
{
+ Guard.NotNull(callback, nameof(callback));
+ Guard.NotNullOrEmpty(property, nameof(property));
+ Guard.NotNull(value, nameof(value));
+
using (Profiler.TraceMethod())
{
var streamName = await projectionClient.CreateProjectionAsync(property, value);
@@ -76,6 +85,8 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default)
{
+ Guard.NotNull(callback, nameof(callback));
+
using (Profiler.TraceMethod())
{
var streamName = await projectionClient.CreateProjectionAsync(streamFilter);
@@ -91,7 +102,7 @@ namespace Squidex.Infrastructure.EventSourcing
StreamEventsSlice currentSlice;
do
{
- currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
+ currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
if (currentSlice.Status == SliceReadStatus.Success)
{
@@ -99,7 +110,7 @@ namespace Squidex.Infrastructure.EventSourcing
foreach (var resolved in currentSlice.Events)
{
- var storedEvent = Formatter.Read(resolved, serializer);
+ var storedEvent = Formatter.Read(resolved, prefix, serializer);
await callback(storedEvent);
}
@@ -110,16 +121,18 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task> QueryAsync(string streamName, long streamPosition = 0)
{
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+
using (Profiler.TraceMethod())
{
var result = new List();
- var sliceStart = streamPosition;
+ var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start;
StreamEventsSlice currentSlice;
do
{
- currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
+ currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true);
if (currentSlice.Status == SliceReadStatus.Success)
{
@@ -127,7 +140,7 @@ namespace Squidex.Infrastructure.EventSourcing
foreach (var resolved in currentSlice.Events)
{
- var storedEvent = Formatter.Read(resolved, serializer);
+ var storedEvent = Formatter.Read(resolved, prefix, serializer);
result.Add(storedEvent);
}
@@ -141,7 +154,9 @@ namespace Squidex.Infrastructure.EventSourcing
public Task DeleteStreamAsync(string streamName)
{
- return connection.DeleteStreamAsync(streamName, ExpectedVersion.Any);
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+
+ return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any);
}
public Task AppendAsync(Guid commitId, string streamName, ICollection events)
@@ -158,40 +173,47 @@ namespace Squidex.Infrastructure.EventSourcing
private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events)
{
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+ Guard.NotNull(events, nameof(events));
+
using (Profiler.TraceMethod(nameof(AppendAsync)))
{
- Guard.NotNullOrEmpty(streamName, nameof(streamName));
- Guard.NotNull(events, nameof(events));
-
if (events.Count == 0)
{
return;
}
- var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList();
-
- if (eventsToSave.Count < WritePageSize)
+ try
{
- await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave);
- }
- else
- {
- using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion))
+ var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList();
+
+ if (eventsToSave.Count < WritePageSize)
+ {
+ await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave);
+ }
+ else
{
- for (var p = 0; p < eventsToSave.Count; p += WritePageSize)
+ using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion))
{
- await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize));
- }
+ for (var p = 0; p < eventsToSave.Count; p += WritePageSize)
+ {
+ await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize));
+ }
- await transaction.CommitAsync();
+ await transaction.CommitAsync();
+ }
}
}
+ catch (WrongExpectedVersionException ex)
+ {
+ throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion);
+ }
}
}
- public Task DeleteManyAsync(string property, object value)
+ private static int ParseVersion(string message)
{
- throw new NotSupportedException();
+ return int.Parse(message.Substring(message.LastIndexOf(':') + 1));
}
private string GetStreamName(string streamName)
diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
index a1d3faf01..0f06e4e77 100644
--- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
+++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
@@ -18,6 +18,7 @@ namespace Squidex.Infrastructure.EventSourcing
private readonly IEventStoreConnection connection;
private readonly IEventSubscriber subscriber;
private readonly IJsonSerializer serializer;
+ private readonly string prefix;
private readonly EventStoreCatchUpSubscription subscription;
private readonly long? position;
@@ -27,13 +28,13 @@ namespace Squidex.Infrastructure.EventSourcing
IJsonSerializer serializer,
ProjectionClient projectionClient,
string position,
+ string prefix,
string streamFilter)
{
- Guard.NotNull(subscriber, nameof(subscriber));
-
this.connection = connection;
this.position = projectionClient.ParsePositionOrNull(position);
+ this.prefix = prefix;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
@@ -61,7 +62,7 @@ namespace Squidex.Infrastructure.EventSourcing
return connection.SubscribeToStreamFrom(streamName, position, settings,
(s, e) =>
{
- var storedEvent = Formatter.Read(e, serializer);
+ var storedEvent = Formatter.Read(e, prefix, serializer);
subscriber.OnEventAsync(this, storedEvent).Wait();
}, null,
diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
index 2873e8625..ca098bb1e 100644
--- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
+++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
@@ -137,7 +137,7 @@ namespace Squidex.Infrastructure.EventSourcing
public long ParsePosition(string position)
{
- return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0;
+ return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start;
}
}
}
diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
index 7cb74737a..f76589938 100644
--- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
+++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
@@ -7,7 +7,6 @@
using System;
using System.IO;
-using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@@ -81,14 +80,14 @@ namespace Squidex.Infrastructure.Assets
}
}
- public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
- return UploadCoreAsync(GetObjectName(id, version, suffix), stream, ct);
+ return UploadCoreAsync(GetObjectName(id, version, suffix), stream, overwrite, ct);
}
public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
{
- return UploadCoreAsync(fileName, stream, ct);
+ return UploadCoreAsync(fileName, stream, false, ct);
}
public Task DeleteAsync(string id, long version, string suffix)
@@ -101,11 +100,11 @@ namespace Squidex.Infrastructure.Assets
return DeleteCoreAsync(fileName);
}
- private async Task UploadCoreAsync(string objectName, Stream stream, CancellationToken ct = default)
+ private async Task UploadCoreAsync(string objectName, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
try
{
- await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream, IfNotExists, ct);
+ await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct);
}
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed)
{
@@ -141,7 +140,7 @@ namespace Squidex.Infrastructure.Assets
private static string GetFileName(string id, long version, string suffix)
{
- return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x)));
+ return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix);
}
}
}
diff --git a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
index e373715a2..997fd9068 100644
--- a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
+++ b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs
@@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Assets
using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct))
{
- await UploadFileCoreAsync(target, readStream, ct);
+ await UploadFileCoreAsync(target, readStream, false, ct);
}
}
catch (GridFSFileNotFoundException ex)
@@ -78,14 +78,14 @@ namespace Squidex.Infrastructure.Assets
}
}
- public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
- return UploadFileCoreAsync(GetFileName(id, version, suffix), stream, ct);
+ return UploadFileCoreAsync(GetFileName(id, version, suffix), stream, overwrite, ct);
}
public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
{
- return UploadFileCoreAsync(fileName, stream, ct);
+ return UploadFileCoreAsync(fileName, stream, false, ct);
}
public Task DeleteAsync(string id, long version, string suffix)
@@ -110,10 +110,15 @@ namespace Squidex.Infrastructure.Assets
}
}
- private async Task UploadFileCoreAsync(string id, Stream stream, CancellationToken ct = default)
+ private async Task UploadFileCoreAsync(string id, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
try
{
+ if (overwrite)
+ {
+ await bucket.DeleteAsync(id, ct);
+ }
+
await bucket.UploadFromStreamAsync(id, id, stream, cancellationToken: ct);
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
@@ -128,7 +133,7 @@ namespace Squidex.Infrastructure.Assets
private static string GetFileName(string id, long version, string suffix)
{
- return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x)));
+ return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix);
}
}
}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
index b9bdcbf10..4f254ddd4 100644
--- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
+++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs
@@ -10,36 +10,43 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
+using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
+using EventFilter = MongoDB.Driver.FilterDefinition;
namespace Squidex.Infrastructure.EventSourcing
{
+ public delegate bool EventPredicate(EventData data);
+
public partial class MongoEventStore : MongoRepositoryBase, IEventStore
{
public Task CreateIndexAsync(string property)
{
+ Guard.NotNullOrEmpty(property, nameof(property));
+
return Collection.Indexes.CreateOneAsync(
new CreateIndexModel(Index.Ascending(CreateIndexPath(property))));
}
- public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
+ public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null)
{
Guard.NotNull(subscriber, nameof(subscriber));
- Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter));
return new PollingSubscription(this, subscriber, streamFilter, position);
}
public async Task> QueryAsync(string streamName, long streamPosition = 0)
{
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+
using (Profiler.TraceMethod())
{
var commits =
await Collection.Find(
Filter.And(
Filter.Eq(EventStreamField, streamName),
- Filter.Gte(EventStreamOffsetField, streamPosition - 1)))
+ Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize)))
.Sort(Sort.Ascending(TimestampField)).ToListAsync();
var result = new List();
@@ -51,13 +58,13 @@ namespace Squidex.Infrastructure.EventSourcing
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
- foreach (var e in commit.Events)
+ foreach (var @event in commit.Events)
{
eventStreamOffset++;
if (eventStreamOffset >= streamPosition)
{
- var eventData = e.ToEventData();
+ var eventData = @event.ToEventData();
var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData));
@@ -72,12 +79,15 @@ namespace Squidex.Infrastructure.EventSourcing
public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default)
{
Guard.NotNull(callback, nameof(callback));
+ Guard.NotNullOrEmpty(property, nameof(property));
+ Guard.NotNull(value, nameof(value));
StreamPosition lastPosition = position;
- var filter = CreateFilter(property, value, lastPosition);
+ var filterDefinition = CreateFilter(property, value, lastPosition);
+ var filterExpression = CreateFilterExpression(property, value);
- return QueryAsync(callback, lastPosition, filter, ct);
+ return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct);
}
public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default)
@@ -86,68 +96,73 @@ namespace Squidex.Infrastructure.EventSourcing
StreamPosition lastPosition = position;
- var filter = CreateFilter(streamFilter, lastPosition);
+ var filterDefinition = CreateFilter(streamFilter, lastPosition);
+ var filterExpression = CreateFilterExpression(null, null);
- return QueryAsync(callback, lastPosition, filter, ct);
+ return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct);
}
- private async Task QueryAsync(Func callback, StreamPosition lastPosition, FilterDefinition filter, CancellationToken ct = default)
+ private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default)
{
using (Profiler.TraceMethod())
{
- await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit =>
+ await Collection.Find(filterDefinition).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit =>
{
var eventStreamOffset = (int)commit.EventStreamOffset;
var commitTimestamp = commit.Timestamp;
var commitOffset = 0;
- foreach (var e in commit.Events)
+ foreach (var @event in commit.Events)
{
eventStreamOffset++;
if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp)
{
- var eventData = e.ToEventData();
- var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
+ var eventData = @event.ToEventData();
- await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData));
+ if (filterExpression(eventData))
+ {
+ var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length);
- commitOffset++;
+ await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData));
+ }
}
+
+ commitOffset++;
}
}, ct);
}
}
- private static FilterDefinition CreateFilter(string property, object value, StreamPosition streamPosition)
+ private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition)
{
- var filters = new List>();
+ var filters = new List();
- FilterByPosition(streamPosition, filters);
- FilterByProperty(property, value, filters);
+ AppendByPosition(streamPosition, filters);
+ AppendByProperty(property, value, filters);
return Filter.And(filters);
}
- private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition)
+ private static EventFilter CreateFilter(string streamFilter, StreamPosition streamPosition)
{
- var filters = new List>();
+ var filters = new List();
- FilterByPosition(streamPosition, filters);
- FilterByStream(streamFilter, filters);
+ AppendByPosition(streamPosition, filters);
+ AppendByStream(streamFilter, filters);
return Filter.And(filters);
}
- private static void FilterByProperty(string property, object value, List> filters)
+ private static void AppendByProperty(string property, object value, List filters)
{
filters.Add(Filter.Eq(CreateIndexPath(property), value));
}
- private static void FilterByStream(string streamFilter, List> filters)
+ private static void AppendByStream(string streamFilter, List filters)
{
- if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase))
+ if (!StreamFilter.IsAll(streamFilter))
{
if (streamFilter.Contains("^"))
{
@@ -160,7 +175,7 @@ namespace Squidex.Infrastructure.EventSourcing
}
}
- private static void FilterByPosition(StreamPosition streamPosition, List> filters)
+ private static void AppendByPosition(StreamPosition streamPosition, List filters)
{
if (streamPosition.IsEndOfCommit)
{
@@ -172,6 +187,20 @@ namespace Squidex.Infrastructure.EventSourcing
}
}
+ private static EventPredicate CreateFilterExpression(string property, object value)
+ {
+ if (!string.IsNullOrWhiteSpace(property))
+ {
+ var jsonValue = JsonValue.Create(value);
+
+ return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue);
+ }
+ else
+ {
+ return x => true;
+ }
+ }
+
private static string CreateIndexPath(string property)
{
return $"Events.Metadata.{property}";
diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
index 4e77f4991..29a9de831 100644
--- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
+++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs
@@ -16,17 +16,15 @@ namespace Squidex.Infrastructure.EventSourcing
{
public partial class MongoEventStore
{
+ private const int MaxCommitSize = 10;
private const int MaxWriteAttempts = 20;
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);
public Task DeleteStreamAsync(string streamName)
{
- return Collection.DeleteManyAsync(x => x.EventStream == streamName);
- }
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
- public Task DeleteManyAsync(string property, object value)
- {
- return Collection.DeleteManyAsync(Filter.Eq(CreateIndexPath(property), value));
+ return Collection.DeleteManyAsync(x => x.EventStream == streamName);
}
public Task AppendAsync(Guid commitId, string streamName, ICollection events)
@@ -36,18 +34,22 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events)
{
+ Guard.NotEmpty(commitId, nameof(commitId));
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+ Guard.NotNull(events, nameof(events));
+ Guard.LessThan(events.Count, MaxCommitSize, "events.Count");
+ Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion));
+ Guard.NotNullOrEmpty(streamName, nameof(streamName));
+ Guard.NotNull(events, nameof(events));
+
using (Profiler.TraceMethod())
{
- Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion));
- Guard.NotNullOrEmpty(streamName, nameof(streamName));
- Guard.NotNull(events, nameof(events));
-
if (events.Count == 0)
{
return;
}
- var currentVersion = await GetEventStreamOffset(streamName);
+ var currentVersion = await GetEventStreamOffsetAsync(streamName);
if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion)
{
@@ -70,7 +72,7 @@ namespace Squidex.Infrastructure.EventSourcing
{
if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
- currentVersion = await GetEventStreamOffset(streamName);
+ currentVersion = await GetEventStreamOffsetAsync(streamName);
if (expectedVersion != EtagVersion.Any)
{
@@ -95,7 +97,7 @@ namespace Squidex.Infrastructure.EventSourcing
}
}
- private async Task GetEventStreamOffset(string streamName)
+ private async Task GetEventStreamOffsetAsync(string streamName)
{
var document =
await Collection.Find(Filter.Eq(EventStreamField, streamName))
diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs
index 45c0910e9..661c83435 100644
--- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs
+++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs
@@ -9,7 +9,7 @@ using MongoDB.Bson;
namespace Squidex.Infrastructure.EventSourcing
{
- public sealed class StreamPosition
+ internal sealed class StreamPosition
{
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs
index 77019a716..7fe870e3c 100644
--- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs
@@ -8,6 +8,7 @@
using System;
using System.Linq;
using System.Reflection;
+using System.Threading;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using Newtonsoft.Json;
@@ -17,36 +18,41 @@ namespace Squidex.Infrastructure.MongoDb
{
public static class BsonJsonConvention
{
+ private static volatile int isRegistered;
+
public static void Register(JsonSerializer serializer)
{
- var pack = new ConventionPack();
-
- pack.AddMemberMapConvention("JsonBson", memberMap =>
+ if (Interlocked.Increment(ref isRegistered) == 1)
{
- var attributes = memberMap.MemberInfo.GetCustomAttributes();
+ var pack = new ConventionPack();
- if (attributes.OfType().Any())
+ pack.AddMemberMapConvention("JsonBson", memberMap =>
{
- var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType);
- var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer);
+ var attributes = memberMap.MemberInfo.GetCustomAttributes();
- memberMap.SetSerializer((IBsonSerializer)bsonSerializer);
- }
- else if (memberMap.MemberType == typeof(JToken))
- {
- memberMap.SetSerializer(JTokenSerializer.Instance);
- }
- else if (memberMap.MemberType == typeof(JObject))
- {
- memberMap.SetSerializer(JTokenSerializer.Instance);
- }
- else if (memberMap.MemberType == typeof(JValue))
- {
- memberMap.SetSerializer(JTokenSerializer.Instance);
- }
- });
+ if (attributes.OfType().Any())
+ {
+ var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType);
+ var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer);
+
+ memberMap.SetSerializer((IBsonSerializer)bsonSerializer);
+ }
+ else if (memberMap.MemberType == typeof(JToken))
+ {
+ memberMap.SetSerializer(JTokenSerializer.Instance);
+ }
+ else if (memberMap.MemberType == typeof(JObject))
+ {
+ memberMap.SetSerializer(JTokenSerializer.Instance);
+ }
+ else if (memberMap.MemberType == typeof(JValue))
+ {
+ memberMap.SetSerializer(JTokenSerializer.Instance);
+ }
+ });
- ConventionRegistry.Register("json", pack, t => true);
+ ConventionRegistry.Register("json", pack, t => true);
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs
index b4a5d5926..6979cafe3 100644
--- a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs
@@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using System;
+using System.Threading;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using NodaTime;
@@ -14,16 +14,14 @@ namespace Squidex.Infrastructure.MongoDb
{
public sealed class InstantSerializer : SerializerBase, IBsonPolymorphicSerializer
{
- private static readonly Lazy Registerer = new Lazy(() =>
- {
- BsonSerializer.RegisterSerializer(new InstantSerializer());
-
- return true;
- });
+ private static volatile int isRegistered;
- public static bool Register()
+ public static void Register()
{
- return !Registerer.IsValueCreated && Registerer.Value;
+ if (Interlocked.Increment(ref isRegistered) == 1)
+ {
+ BsonSerializer.RegisterSerializer(new InstantSerializer());
+ }
}
public bool IsDiscriminatorCompatibleWithObjectSerializer
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs
new file mode 100644
index 000000000..65462dc6c
--- /dev/null
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure.MongoDb
+{
+ public sealed class MongoDbOptions
+ {
+ public bool IsCosmosDb { get; set; }
+ }
+}
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
index 631727d92..ec0a29528 100644
--- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
@@ -22,6 +22,16 @@ namespace Squidex.Infrastructure.MongoDb
{
private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true };
+ public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName)
+ {
+ var options = new ListCollectionNamesOptions
+ {
+ Filter = new BsonDocument("name", collectionName)
+ };
+
+ return (await database.ListCollectionNamesAsync(options)).Any();
+ }
+
public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document)
{
try
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
index 00237ee83..689bdfe8e 100644
--- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
@@ -44,6 +44,7 @@ namespace Squidex.Infrastructure.MongoDb
static MongoRepositoryBase()
{
RefTokenSerializer.Register();
+
InstantSerializer.Register();
}
diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs
index 6f8761148..b4c45f945 100644
--- a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs
+++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs
@@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using System;
+using System.Threading;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
@@ -13,16 +13,14 @@ namespace Squidex.Infrastructure.MongoDb
{
public class RefTokenSerializer : ClassSerializerBase
{
- private static readonly Lazy Registerer = new Lazy(() =>
- {
- BsonSerializer.RegisterSerializer(new RefTokenSerializer());
-
- return true;
- });
+ private static volatile int isRegistered;
- public static bool Register()
+ public static void Register()
{
- return !Registerer.IsValueCreated && Registerer.Value;
+ if (Interlocked.Increment(ref isRegistered) == 1)
+ {
+ BsonSerializer.RegisterSerializer(new RefTokenSerializer());
+ }
}
protected override RefToken DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
index 683baf450..0acd60699 100644
--- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
+++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
@@ -12,8 +12,8 @@
-
-
+
+
diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
index 3f0a356b1..61afdd1cf 100644
--- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
+++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
@@ -5,10 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
+using Newtonsoft.Json;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
@@ -16,9 +19,12 @@ namespace Squidex.Infrastructure.States
{
public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore
{
- public MongoSnapshotStore(IMongoDatabase database)
+ public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer)
: base(database)
{
+ Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
+
+ BsonJsonConvention.Register(jsonSerializer);
}
protected override string CollectionName()
@@ -55,11 +61,11 @@ namespace Squidex.Infrastructure.States
}
}
- public async Task ReadAllAsync(System.Func callback)
+ public async Task ReadAllAsync(Func callback, CancellationToken ct = default)
{
using (Profiler.TraceMethod>())
{
- await Collection.Find(new BsonDocument()).ForEachAsync(x => callback(x.Doc, x.Version));
+ await Collection.Find(new BsonDocument()).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct);
}
}
diff --git a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs
index 18a544342..21d44ae47 100644
--- a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs
+++ b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs
@@ -49,8 +49,8 @@ namespace Squidex.Infrastructure.CQRS.Events
this.exchange = exchange;
this.eventsFilter = eventsFilter;
- this.jsonSerializer = jsonSerializer;
this.eventPublisherName = eventPublisherName;
+ this.jsonSerializer = jsonSerializer;
}
protected override void DisposeObject(bool disposing)
diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs
index 3feaf1510..74ef5c100 100644
--- a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs
+++ b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs
@@ -19,22 +19,19 @@ namespace Squidex.Infrastructure
public sealed class RedisPubSub : IPubSub, IInitializable
{
private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary();
- private readonly Lazy redisClient;
+ private readonly IConnectionMultiplexer redis;
private readonly IJsonSerializer serializer;
- private readonly Lazy redisSubscriber;
private readonly ISemanticLog log;
+ private ISubscriber redisSubscriber;
- public RedisPubSub(Lazy redis, IJsonSerializer serializer, ISemanticLog log)
+ public RedisPubSub(IConnectionMultiplexer redis, IJsonSerializer serializer, ISemanticLog log)
{
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(redis, nameof(redis));
Guard.NotNull(log, nameof(log));
this.log = log;
-
- redisClient = redis;
- redisSubscriber = new Lazy(() => redis.Value.GetSubscriber());
-
+ this.redis = redis;
this.serializer = serializer;
}
@@ -42,13 +39,15 @@ namespace Squidex.Infrastructure
{
try
{
- redisClient.Value.GetStatus();
+ redisSubscriber = redis.GetSubscriber();
+
+ redis.GetStatus();
return TaskHelper.Done;
}
catch (Exception ex)
{
- throw new ConfigurationException($"Redis connection failed to connect to database {redisClient.Value.Configuration}", ex);
+ throw new ConfigurationException($"Redis connection failed to connect to database {redis.Configuration}", ex);
}
}
@@ -66,7 +65,7 @@ namespace Squidex.Infrastructure
{
var typeName = typeof(T).FullName;
- return (RedisSubscription)subscriptions.GetOrAdd(typeName, this, (k, c) => new RedisSubscription(c.redisSubscriber.Value, serializer, k, c.log));
+ return (RedisSubscription)subscriptions.GetOrAdd(typeName, this, (k, c) => new RedisSubscription(c.redisSubscriber, serializer, k, c.log));
}
}
}
diff --git a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs
index 954f26c4c..ddf8465e0 100644
--- a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs
+++ b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs
@@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Assets
{
Guard.NotNullOrEmpty(fileName, nameof(fileName));
- return $"An asset with name '{fileName}' already not exists.";
+ return $"An asset with name '{fileName}' already exists.";
}
}
}
diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
index 3bcf6ef5e..a61e24366 100644
--- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
+++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs
@@ -7,7 +7,6 @@
using System;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
@@ -96,14 +95,14 @@ namespace Squidex.Infrastructure.Assets
}
}
- public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
- return UploadCoreAsync(GetFile(id, version, suffix), stream, ct);
+ return UploadCoreAsync(GetFile(id, version, suffix), stream, overwrite, ct);
}
public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
{
- return UploadCoreAsync(GetFile(fileName), stream, ct);
+ return UploadCoreAsync(GetFile(fileName), stream, false, ct);
}
public Task DeleteAsync(string id, long version, string suffix)
@@ -123,11 +122,11 @@ namespace Squidex.Infrastructure.Assets
return TaskHelper.Done;
}
- private static async Task UploadCoreAsync(FileInfo file, Stream stream, CancellationToken ct = default)
+ private static async Task UploadCoreAsync(FileInfo file, Stream stream, bool overwrite = false, CancellationToken ct = default)
{
try
{
- using (var fileStream = file.Open(FileMode.CreateNew, FileAccess.Write))
+ using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write))
{
await stream.CopyToAsync(fileStream, BufferSize, ct);
}
@@ -159,7 +158,7 @@ namespace Squidex.Infrastructure.Assets
private string GetPath(string id, long version, string suffix)
{
- return Path.Combine(directory.FullName, string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))));
+ return Path.Combine(directory.FullName, StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix));
}
}
}
diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs
index b4170bfff..65d3c4f84 100644
--- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs
+++ b/src/Squidex.Infrastructure/Assets/IAssetStore.cs
@@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Assets
Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default);
- Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default);
+ Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default);
Task DeleteAsync(string fileName);
diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs
new file mode 100644
index 000000000..bc3b8804e
--- /dev/null
+++ b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs
@@ -0,0 +1,135 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Concurrent;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Squidex.Infrastructure.Tasks;
+
+namespace Squidex.Infrastructure.Assets
+{
+ public sealed class MemoryAssetStore : IAssetStore
+ {
+ private readonly ConcurrentDictionary streams = new ConcurrentDictionary();
+ private readonly AsyncLock readerLock = new AsyncLock();
+ private readonly AsyncLock writerLock = new AsyncLock();
+
+ public string GeneratePublicUrl(string id, long version, string suffix)
+ {
+ return null;
+ }
+
+ public async Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default)
+ {
+ Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName));
+ Guard.NotNullOrEmpty(id, nameof(id));
+
+ if (!streams.TryGetValue(sourceFileName, out var sourceStream))
+ {
+ throw new AssetNotFoundException(sourceFileName);
+ }
+
+ using (await readerLock.LockAsync())
+ {
+ await UploadAsync(id, version, suffix, sourceStream, false, ct);
+ }
+ }
+
+ public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ {
+ Guard.NotNullOrEmpty(id, nameof(id));
+
+ var fileName = GetFileName(id, version, suffix);
+
+ if (!streams.TryGetValue(fileName, out var sourceStream))
+ {
+ throw new AssetNotFoundException(fileName);
+ }
+
+ using (await readerLock.LockAsync())
+ {
+ try
+ {
+ await sourceStream.CopyToAsync(stream, 81920, ct);
+ }
+ finally
+ {
+ sourceStream.Position = 0;
+ }
+ }
+ }
+
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
+ {
+ Guard.NotNullOrEmpty(id, nameof(id));
+
+ return UploadCoreAsync(GetFileName(id, version, suffix), stream, overwrite, ct);
+ }
+
+ public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
+ {
+ return UploadCoreAsync(fileName, stream, false);
+ }
+
+ private async Task UploadCoreAsync(string fileName, Stream stream, bool overwrite, CancellationToken ct = default)
+ {
+ var memoryStream = new MemoryStream();
+
+ async Task CopyAsync()
+ {
+ using (await writerLock.LockAsync())
+ {
+ try
+ {
+ await stream.CopyToAsync(memoryStream, 81920, ct);
+ }
+ finally
+ {
+ memoryStream.Position = 0;
+ }
+ }
+ }
+
+ if (overwrite)
+ {
+ await CopyAsync();
+
+ streams[fileName] = memoryStream;
+ }
+ else if (streams.TryAdd(fileName, memoryStream))
+ {
+ await CopyAsync();
+ }
+ else
+ {
+ throw new AssetAlreadyExistsException(fileName);
+ }
+ }
+
+ public Task DeleteAsync(string id, long version, string suffix)
+ {
+ Guard.NotNullOrEmpty(id, nameof(id));
+
+ return DeleteAsync(GetFileName(id, version, suffix));
+ }
+
+ public Task DeleteAsync(string fileName)
+ {
+ Guard.NotNullOrEmpty(fileName, nameof(fileName));
+
+ streams.TryRemove(fileName, out _);
+
+ return TaskHelper.Done;
+ }
+
+ private string GetFileName(string id, long version, string suffix)
+ {
+ return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix);
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs
new file mode 100644
index 000000000..b48eb7269
--- /dev/null
+++ b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs
@@ -0,0 +1,52 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Squidex.Infrastructure.Assets
+{
+ public sealed class NoopAssetStore : IAssetStore
+ {
+ public string GeneratePublicUrl(string id, long version, string suffix)
+ {
+ return null;
+ }
+
+ public Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string fileName)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string id, long version, string suffix)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure/Lazier.cs b/src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs
similarity index 66%
rename from src/Squidex.Infrastructure/Lazier.cs
rename to src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs
index aae97240c..1cb4f5ee5 100644
--- a/src/Squidex.Infrastructure/Lazier.cs
+++ b/src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs
@@ -5,16 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using System;
-using Microsoft.Extensions.DependencyInjection;
-
namespace Squidex.Infrastructure
{
- public sealed class Lazier : Lazy where T : class
+ public sealed class AutoAssembyTypeProvider : ITypeProvider
{
- public Lazier(IServiceProvider provider)
- : base(provider.GetRequiredService)
+ public void Map(TypeNameRegistry typeNameRegistry)
{
+ typeNameRegistry.MapUnmapped(typeof(T).Assembly);
}
}
}
diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
index 16da5a476..21d4a7436 100644
--- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
+++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
@@ -13,7 +13,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
- public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new()
+ public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new()
{
private readonly IStore store;
private T snapshot = new T { Version = EtagVersion.Empty };
@@ -66,6 +66,9 @@ namespace Squidex.Infrastructure.Commands
}
}
- protected abstract T OnEvent(Envelope @event);
+ protected T OnEvent(Envelope @event)
+ {
+ return Snapshot.Apply(@event);
+ }
}
}
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
index 61501f7ac..5c3b0d717 100644
--- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
+++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
@@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Commands
{
- public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new()
+ public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new()
{
private readonly List> uncomittedEvents = new List>();
private readonly ISemanticLog log;
diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/src/Squidex.Infrastructure/Commands/IDomainState.cs
index ee6891fe4..f20f14ce8 100644
--- a/src/Squidex.Infrastructure/Commands/IDomainState.cs
+++ b/src/Squidex.Infrastructure/Commands/IDomainState.cs
@@ -5,10 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using Squidex.Infrastructure.EventSourcing;
+
namespace Squidex.Infrastructure.Commands
{
- public interface IDomainState
+ public interface IDomainState
{
long Version { get; set; }
+
+ T Apply(Envelope @event);
}
}
diff --git a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
index 10ab25586..8e820282f 100644
--- a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
+++ b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
@@ -15,7 +15,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
- public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new()
+ public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new()
{
private readonly IStore store;
private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } };
@@ -88,6 +88,9 @@ namespace Squidex.Infrastructure.Commands
}
}
- protected abstract T OnEvent(Envelope @event);
+ protected T OnEvent(Envelope @event)
+ {
+ return Snapshot.Apply(@event);
+ }
}
}
\ No newline at end of file
diff --git a/src/Squidex/Config/Options.cs b/src/Squidex.Infrastructure/Configuration/Alternatives.cs
similarity index 70%
rename from src/Squidex/Config/Options.cs
rename to src/Squidex.Infrastructure/Configuration/Alternatives.cs
index 1962bba5e..77d70602f 100644
--- a/src/Squidex/Config/Options.cs
+++ b/src/Squidex.Infrastructure/Configuration/Alternatives.cs
@@ -1,18 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
-namespace Squidex.Config
+namespace Microsoft.Extensions.Configuration
{
- public sealed class Options : Dictionary
+ public sealed class Alternatives : Dictionary
{
- public Options()
+ public Alternatives()
: base(StringComparer.OrdinalIgnoreCase)
{
}
diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs
new file mode 100644
index 000000000..4c86b80a0
--- /dev/null
+++ b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs
@@ -0,0 +1,69 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Globalization;
+using System.Linq;
+using Squidex.Infrastructure;
+
+namespace Microsoft.Extensions.Configuration
+{
+ public static class ConfigurationExtensions
+ {
+ public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default)
+ {
+ var value = config.GetValue(path, defaultValue);
+
+ return value;
+ }
+
+ public static int GetOptionalValue(this IConfiguration config, string path, int defaultValue)
+ {
+ var value = config.GetValue(path);
+
+ if (string.IsNullOrWhiteSpace(value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
+ {
+ result = defaultValue;
+ }
+
+ return result;
+ }
+
+ public static string GetRequiredValue(this IConfiguration config, string path)
+ {
+ var value = config.GetValue(path);
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ var name = string.Join(" ", path.Split(':').Select(x => x.ToPascalCase()));
+
+ throw new ConfigurationException($"Configure the {name} with '{path}'.");
+ }
+
+ return value;
+ }
+
+ public static string ConfigureByOption(this IConfiguration config, string path, Alternatives options)
+ {
+ var value = config.GetRequiredValue(path);
+
+ if (options.TryGetValue(value, out var action))
+ {
+ action();
+ }
+ else if (options.TryGetValue("default", out action))
+ {
+ action();
+ }
+ else
+ {
+ throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(" ", options.Keys)}.");
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Squidex/Config/ServiceExtensions.cs b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs
similarity index 53%
rename from src/Squidex/Config/ServiceExtensions.cs
rename to src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs
index 8d185bf60..9ee638ac0 100644
--- a/src/Squidex/Config/ServiceExtensions.cs
+++ b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs
@@ -1,20 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
-using System.Globalization;
using System.Linq;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Squidex.Infrastructure;
-namespace Squidex.Config
+namespace Microsoft.Extensions.DependencyInjection
{
- public static class ServiceExtensions
+ public static class DependencyInjectionExtensions
{
public sealed class InterfaceRegistrator
{
@@ -30,6 +28,16 @@ namespace Squidex.Config
return this;
}
+ public InterfaceRegistrator AsOptional()
+ {
+ if (typeof(TInterface) != typeof(T))
+ {
+ services.TryAddSingleton(typeof(TInterface), c => c.GetRequiredService());
+ }
+
+ return this;
+ }
+
public InterfaceRegistrator As()
{
if (typeof(TInterface) != typeof(T))
@@ -64,15 +72,6 @@ namespace Squidex.Config
return new InterfaceRegistrator(services);
}
- public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, T instance) where T : class
- {
- services.AddSingleton(typeof(T), instance);
-
- RegisterDefaults(services);
-
- return new InterfaceRegistrator(services);
- }
-
public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class
{
services.AddSingleton();
@@ -84,60 +83,17 @@ namespace Squidex.Config
private static void RegisterDefaults(IServiceCollection services) where T : class
{
- if (typeof(T).GetInterfaces().Contains(typeof(IInitializable)))
- {
- services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService());
- }
- }
-
- public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default)
- {
- var value = config.GetValue(path, defaultValue);
+ var interfaces = typeof(T).GetInterfaces();
- return value;
- }
-
- public static int GetOptionalValue(this IConfiguration config, string path, int defaultValue)
- {
- var value = config.GetValue(path);
- var result = defaultValue;
-
- if (string.IsNullOrWhiteSpace(value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
- {
- result = defaultValue;
- }
-
- return result;
- }
-
- public static string GetRequiredValue(this IConfiguration config, string path)
- {
- var value = config.GetValue(path);
-
- if (string.IsNullOrWhiteSpace(value))
+ if (interfaces.Contains(typeof(IInitializable)))
{
- var name = string.Join(' ', path.Split(':').Select(x => x.ToPascalCase()));
-
- throw new ConfigurationException($"Configure the {name} with '{path}'.");
+ services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService());
}
- return value;
- }
-
- public static string ConfigureByOption(this IConfiguration config, string path, Options options)
- {
- var value = config.GetRequiredValue(path);
-
- if (options.TryGetValue(value, out var action))
+ if (interfaces.Contains(typeof(IBackgroundProcess)))
{
- action();
+ services.AddSingleton(typeof(IBackgroundProcess), c => c.GetRequiredService());
}
- else
- {
- throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(' ', options.Keys)}.");
- }
-
- return value;
}
}
}
diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
index 5e0ed5067..6ee608632 100644
--- a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
+++ b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs
@@ -28,8 +28,6 @@ namespace Squidex.Infrastructure.EventSourcing
Task DeleteStreamAsync(string streamName);
- Task DeleteManyAsync(string property, object value);
-
- IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null);
+ IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null);
}
}
diff --git a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs
new file mode 100644
index 000000000..b3bc063af
--- /dev/null
+++ b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs
@@ -0,0 +1,22 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+
+namespace Squidex.Infrastructure.EventSourcing
+{
+ public static class StreamFilter
+ {
+ public static bool IsAll(string filter)
+ {
+ return string.IsNullOrWhiteSpace(filter)
+ || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure/IBackgroundProcess.cs b/src/Squidex.Infrastructure/IBackgroundProcess.cs
new file mode 100644
index 000000000..38003910c
--- /dev/null
+++ b/src/Squidex.Infrastructure/IBackgroundProcess.cs
@@ -0,0 +1,17 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Squidex.Infrastructure
+{
+ public interface IBackgroundProcess
+ {
+ Task StartAsync(CancellationToken ct);
+ }
+}
diff --git a/src/Squidex.Infrastructure/ITypeProvider.cs b/src/Squidex.Infrastructure/ITypeProvider.cs
new file mode 100644
index 000000000..63ee58238
--- /dev/null
+++ b/src/Squidex.Infrastructure/ITypeProvider.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure
+{
+ public interface ITypeProvider
+ {
+ void Map(TypeNameRegistry typeNameRegistry);
+ }
+}
diff --git a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs
index 9763aabf8..9d13c43b8 100644
--- a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs
+++ b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs
@@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Log
private readonly string applicationSessionId;
public ApplicationInfoLogAppender(Type type, Guid applicationSession)
- : this(type?.GetTypeInfo().Assembly, applicationSession)
+ : this(type?.Assembly, applicationSession)
{
}
diff --git a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
index 7c232bdf6..1b58a9d02 100644
--- a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
+++ b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
@@ -126,8 +126,16 @@ namespace Squidex.Infrastructure.Log
return writer.WriteObject(nameof(exception), exception, (ctx, w) =>
{
w.WriteProperty("type", ctx.GetType().FullName);
- w.WriteProperty("message", ctx.Message);
- w.WriteProperty("stackTrace", ctx.StackTrace);
+
+ if (ctx.Message != null)
+ {
+ w.WriteProperty("message", ctx.Message);
+ }
+
+ if (ctx.StackTrace != null)
+ {
+ w.WriteProperty("stackTrace", ctx.StackTrace);
+ }
});
}
diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs
similarity index 79%
rename from src/Squidex.Infrastructure/Orleans/Bootstrap.cs
rename to src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs
index 5a3bb7a19..ae47858a5 100644
--- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs
+++ b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs
@@ -12,19 +12,19 @@ using Orleans.Runtime;
namespace Squidex.Infrastructure.Orleans
{
- public sealed class Bootstrap : IStartupTask where T : IBackgroundGrain
+ public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain
{
private const int NumTries = 10;
private readonly IGrainFactory grainFactory;
- public Bootstrap(IGrainFactory grainFactory)
+ public GrainBootstrap(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
- public async Task Execute(CancellationToken cancellationToken)
+ public async Task StartAsync(CancellationToken ct = default)
{
for (var i = 1; i <= NumTries; i++)
{
@@ -45,5 +45,10 @@ namespace Squidex.Infrastructure.Orleans
}
}
}
+
+ public override string ToString()
+ {
+ return typeof(T).ToString();
+ }
}
}
diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs
index 0a816c381..a179c78d8 100644
--- a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs
+++ b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs
@@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.Orleans
{
public Guid Key { get; private set; }
- public sealed override Task OnActivateAsync()
+ public override Task OnActivateAsync()
{
return ActivateAsync(this.GetPrimaryKey());
}
diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs
index 041f67df5..13d737ae0 100644
--- a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs
+++ b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs
@@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.Orleans
{
public string Key { get; private set; }
- public sealed override Task OnActivateAsync()
+ public override Task OnActivateAsync()
{
return ActivateAsync(this.GetPrimaryKeyString());
}
diff --git a/src/Squidex.Infrastructure/Plugins/IPlugin.cs b/src/Squidex.Infrastructure/Plugins/IPlugin.cs
new file mode 100644
index 000000000..9d5a2618e
--- /dev/null
+++ b/src/Squidex.Infrastructure/Plugins/IPlugin.cs
@@ -0,0 +1,17 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Squidex.Infrastructure.Plugins
+{
+ public interface IPlugin
+ {
+ void ConfigureServices(IServiceCollection services, IConfiguration config);
+ }
+}
diff --git a/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs b/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs
new file mode 100644
index 000000000..f93e3eadb
--- /dev/null
+++ b/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs
@@ -0,0 +1,16 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.AspNetCore.Builder;
+
+namespace Squidex.Infrastructure.Plugins
+{
+ public interface IWebPlugin : IPlugin
+ {
+ void Configure(IApplicationBuilder app);
+ }
+}
diff --git a/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/src/Squidex.Infrastructure/Plugins/PluginManager.cs
new file mode 100644
index 000000000..34c809c69
--- /dev/null
+++ b/src/Squidex.Infrastructure/Plugins/PluginManager.cs
@@ -0,0 +1,114 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Squidex.Infrastructure.Log;
+
+namespace Squidex.Infrastructure.Plugins
+{
+ public sealed class PluginManager
+ {
+ private readonly HashSet loadedPlugins = new HashSet();
+ private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>();
+
+ public IReadOnlyCollection Plugins
+ {
+ get { return loadedPlugins; }
+ }
+
+ public void Add(string name, Assembly assembly)
+ {
+ Guard.NotNull(assembly, nameof(assembly));
+
+ var pluginTypes =
+ assembly.GetTypes()
+ .Where(t => typeof(IPlugin).IsAssignableFrom(t))
+ .Where(t => !t.IsAbstract);
+
+ foreach (var pluginType in pluginTypes)
+ {
+ try
+ {
+ var plugin = (IPlugin)Activator.CreateInstance(pluginType);
+
+ loadedPlugins.Add(plugin);
+ }
+ catch (Exception ex)
+ {
+ LogException(name, "Instantiating", ex);
+ }
+ }
+ }
+
+ public void LogException(string plugin, string action, Exception exception)
+ {
+ Guard.NotNull(plugin, nameof(plugin));
+ Guard.NotNull(action, nameof(action));
+ Guard.NotNull(exception, nameof(exception));
+
+ exceptions.Add((plugin, action, exception));
+ }
+
+ public void ConfigureServices(IServiceCollection services, IConfiguration config)
+ {
+ Guard.NotNull(services, nameof(services));
+ Guard.NotNull(config, nameof(config));
+
+ foreach (var plugin in loadedPlugins)
+ {
+ plugin.ConfigureServices(services, config);
+ }
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ Guard.NotNull(app, nameof(app));
+
+ foreach (var plugin in loadedPlugins.OfType())
+ {
+ plugin.Configure(app);
+ }
+ }
+
+ public void Log(ISemanticLog log)
+ {
+ Guard.NotNull(log, nameof(log));
+
+ if (loadedPlugins.Count > 0 || exceptions.Count > 0)
+ {
+ var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed";
+
+ log.LogInformation(w => w
+ .WriteProperty("action", "pluginsLoaded")
+ .WriteProperty("status", status)
+ .WriteArray("errors", e =>
+ {
+ foreach (var error in exceptions)
+ {
+ e.WriteObject(x => x
+ .WriteProperty("plugin", error.Plugin)
+ .WriteProperty("action", error.Action)
+ .WriteException(error.Exception));
+ }
+ })
+ .WriteArray("plugins", a =>
+ {
+ foreach (var plugin in loadedPlugins)
+ {
+ a.WriteValue(plugin.GetType().ToString());
+ }
+ }));
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Infrastructure/Plugins/PluginOptions.cs b/src/Squidex.Infrastructure/Plugins/PluginOptions.cs
new file mode 100644
index 000000000..561e0845a
--- /dev/null
+++ b/src/Squidex.Infrastructure/Plugins/PluginOptions.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure.Plugins
+{
+ public sealed class PluginOptions
+ {
+ public string[] Plugins { get; set; }
+ }
+}
diff --git a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs
index 474f55355..55354eb13 100644
--- a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs
+++ b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs
@@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Reflection
BindingFlags.Public |
BindingFlags.Instance;
- if (!type.GetTypeInfo().IsInterface)
+ if (!type.IsInterface)
{
return type.GetProperties(bindingFlags);
}
diff --git a/src/Squidex.Infrastructure/Singletons.cs b/src/Squidex.Infrastructure/Singletons.cs
index 494157155..e0eb715ab 100644
--- a/src/Squidex.Infrastructure/Singletons.cs
+++ b/src/Squidex.Infrastructure/Singletons.cs
@@ -18,10 +18,5 @@ namespace Squidex.Infrastructure
{
return Instances.GetOrAdd(key, factory);
}
-
- public static Lazy GetOrAddLazy(string key, Func factory)
- {
- return new Lazy(() => Instances.GetOrAdd(key, factory));
- }
}
}
diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
index ea5c0a8b0..2ac2b92d0 100644
--- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
+++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
@@ -8,16 +8,17 @@
True
+
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
+
+
@@ -26,7 +27,7 @@
-
+
diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/src/Squidex.Infrastructure/SquidexInfrastructure.cs
index 8dac58f91..a76b688dc 100644
--- a/src/Squidex.Infrastructure/SquidexInfrastructure.cs
+++ b/src/Squidex.Infrastructure/SquidexInfrastructure.cs
@@ -7,9 +7,11 @@
using System.Reflection;
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
namespace Squidex.Infrastructure
{
- public static class SquidexInfrastructure
+ public sealed class SquidexInfrastructure
{
public static readonly Assembly Assembly = typeof(SquidexInfrastructure).Assembly;
}
diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs
index 38646e64f..68243db74 100644
--- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs
+++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.States
@@ -20,6 +21,6 @@ namespace Squidex.Infrastructure.States
Task RemoveAsync(TKey key);
- Task ReadAllAsync(Func callback);
+ Task ReadAllAsync(Func callback, CancellationToken ct = default);
}
}
diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs
index a1c3b100d..eb0c309bd 100644
--- a/src/Squidex.Infrastructure/StringExtensions.cs
+++ b/src/Squidex.Infrastructure/StringExtensions.cs
@@ -16,9 +16,11 @@ namespace Squidex.Infrastructure
public static class StringExtensions
{
private const char NullChar = (char)0;
+
private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled);
private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled);
private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled);
+
private static readonly Dictionary LowerCaseDiacritics;
private static readonly Dictionary Diacritics = new Dictionary
{
@@ -555,6 +557,8 @@ namespace Squidex.Infrastructure
public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false)
{
+ Guard.NotNull(path, nameof(path));
+
var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}";
if (trailingSlash &&
@@ -567,5 +571,34 @@ namespace Squidex.Infrastructure
return url;
}
+
+ public static string JoinNonEmpty(string separator, params string[] parts)
+ {
+ Guard.NotNull(separator, nameof(separator));
+
+ if (parts == null || parts.Length == 0)
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder();
+
+ for (var i = 0; i < parts.Length; i++)
+ {
+ var part = parts[i];
+
+ if (!string.IsNullOrWhiteSpace(part))
+ {
+ sb.Append(part);
+
+ if (i < parts.Length - 1)
+ {
+ sb.Append(separator);
+ }
+ }
+ }
+
+ return sb.ToString();
+ }
}
}
diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs
index 877e76d7a..5409faca1 100644
--- a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs
+++ b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs
@@ -10,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
using Squidex.Infrastructure.Json;
namespace Squidex.Infrastructure.Translations
@@ -18,7 +19,7 @@ namespace Squidex.Infrastructure.Translations
{
private const string Url = "https://api.deepl.com/v2/translate";
private readonly HttpClient httpClient = new HttpClient();
- private readonly string authKey;
+ private readonly DeepLTranslatorOptions deepLOptions;
private readonly IJsonSerializer jsonSerializer;
private sealed class Response
@@ -31,12 +32,12 @@ namespace Squidex.Infrastructure.Translations
public string Text { get; set; }
}
- public DeepLTranslator(string authKey, IJsonSerializer jsonSerializer)
+ public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer)
{
- Guard.NotNull(authKey, nameof(authKey));
+ Guard.NotNull(deepLOptions, nameof(deepLOptions));
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
- this.authKey = authKey;
+ this.deepLOptions = deepLOptions.Value;
this.jsonSerializer = jsonSerializer;
}
@@ -48,9 +49,14 @@ namespace Squidex.Infrastructure.Translations
return new Translation(TranslationResult.NotTranslated, sourceText);
}
+ if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey))
+ {
+ return new Translation(TranslationResult.NotImplemented);
+ }
+
var parameters = new Dictionary
{
- ["auth_key"] = authKey,
+ ["auth_key"] = deepLOptions.AuthKey,
["text"] = sourceText,
["target_lang"] = GetLanguageCode(targetLanguage)
};
@@ -81,7 +87,7 @@ namespace Squidex.Infrastructure.Translations
return new Translation(TranslationResult.Failed, resultText: responseString);
}
- private string GetLanguageCode(Language language)
+ private static string GetLanguageCode(Language language)
{
return language.Iso2Code.Substring(0, 2).ToUpperInvariant();
}
diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs
new file mode 100644
index 000000000..d7124e343
--- /dev/null
+++ b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Infrastructure.Translations
+{
+ public sealed class DeepLTranslatorOptions
+ {
+ public string AuthKey { get; set; }
+ }
+}
diff --git a/src/Squidex.Infrastructure/TypeNameRegistry.cs b/src/Squidex.Infrastructure/TypeNameRegistry.cs
index 233f5ee28..5df5f9f00 100644
--- a/src/Squidex.Infrastructure/TypeNameRegistry.cs
+++ b/src/Squidex.Infrastructure/TypeNameRegistry.cs
@@ -16,6 +16,17 @@ namespace Squidex.Infrastructure
private readonly Dictionary namesByType = new Dictionary();
private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ public TypeNameRegistry(IEnumerable providers = null)
+ {
+ if (providers != null)
+ {
+ foreach (var provider in providers)
+ {
+ Map(provider);
+ }
+ }
+ }
+
public TypeNameRegistry MapObsolete(Type type, string name)
{
Guard.NotNull(type, nameof(type));
@@ -36,11 +47,20 @@ namespace Squidex.Infrastructure
return this;
}
+ public TypeNameRegistry Map(ITypeProvider provider)
+ {
+ Guard.NotNull(provider, nameof(provider));
+
+ provider.Map(this);
+
+ return this;
+ }
+
public TypeNameRegistry Map(Type type)
{
Guard.NotNull(type, nameof(type));
- var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute();
+ var typeNameAttribute = type.GetCustomAttribute();
if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName))
{
diff --git a/src/Squidex.Web/ApiController.cs b/src/Squidex.Web/ApiController.cs
new file mode 100644
index 000000000..5eb10a7bf
--- /dev/null
+++ b/src/Squidex.Web/ApiController.cs
@@ -0,0 +1,60 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Squidex.Domain.Apps.Entities.Apps;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
+
+namespace Squidex.Web
+{
+ [Area("Api")]
+ [ApiController]
+ [ApiExceptionFilter]
+ [ApiModelValidation(false)]
+ public abstract class ApiController : Controller
+ {
+ protected ICommandBus CommandBus { get; }
+
+ protected IAppEntity App
+ {
+ get
+ {
+ var appFeature = HttpContext.Features.Get();
+
+ if (appFeature == null)
+ {
+ throw new InvalidOperationException("Not in a app context.");
+ }
+
+ return appFeature.App;
+ }
+ }
+
+ protected Guid AppId
+ {
+ get { return App.Id; }
+ }
+
+ protected ApiController(ICommandBus commandBus)
+ {
+ Guard.NotNull(commandBus, nameof(commandBus));
+
+ CommandBus = commandBus;
+ }
+
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ if (!context.HttpContext.Request.PathBase.StartsWithSegments("/api"))
+ {
+ context.Result = new RedirectResult("/");
+ }
+ }
+ }
+}
diff --git a/src/Squidex/Pipeline/ApiCostsAttribute.cs b/src/Squidex.Web/ApiCostsAttribute.cs
similarity index 93%
rename from src/Squidex/Pipeline/ApiCostsAttribute.cs
rename to src/Squidex.Web/ApiCostsAttribute.cs
index 07cc48f18..0b07afc23 100644
--- a/src/Squidex/Pipeline/ApiCostsAttribute.cs
+++ b/src/Squidex.Web/ApiCostsAttribute.cs
@@ -7,8 +7,9 @@
using System;
using Microsoft.AspNetCore.Mvc;
+using Squidex.Web.Pipeline;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class ApiCostsAttribute : ServiceFilterAttribute, IApiCostsFeature
diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex.Web/ApiExceptionFilterAttribute.cs
similarity index 86%
rename from src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
rename to src/Squidex.Web/ApiExceptionFilterAttribute.cs
index a681860ad..3e195c0be 100644
--- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
+++ b/src/Squidex.Web/ApiExceptionFilterAttribute.cs
@@ -7,12 +7,13 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Security;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
@@ -60,7 +61,7 @@ namespace Squidex.Pipeline
private static IActionResult OnValidationException(ValidationException ex)
{
- return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ex.Errors?.ToArray(e => e.Message) });
+ return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) });
}
private static IActionResult ErrorResult(int statusCode, ErrorDto error)
@@ -89,5 +90,20 @@ namespace Squidex.Pipeline
context.Result = result;
}
}
+
+ private static string[] ToDetails(ValidationException ex)
+ {
+ return ex.Errors?.ToArray(e =>
+ {
+ if (e.PropertyNames?.Any() == true)
+ {
+ return $"{string.Join(", ", e.PropertyNames)}: {e.Message}";
+ }
+ else
+ {
+ return e.Message;
+ }
+ });
+ }
}
}
diff --git a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs b/src/Squidex.Web/ApiModelValidationAttribute.cs
similarity index 98%
rename from src/Squidex/Pipeline/ApiModelValidationAttribute.cs
rename to src/Squidex.Web/ApiModelValidationAttribute.cs
index 41bb86156..df1a5a994 100644
--- a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs
+++ b/src/Squidex.Web/ApiModelValidationAttribute.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using Squidex.Infrastructure;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ApiModelValidationAttribute : ActionFilterAttribute
{
diff --git a/src/Squidex/Pipeline/ApiPermissionAttribute.cs b/src/Squidex.Web/ApiPermissionAttribute.cs
similarity index 88%
rename from src/Squidex/Pipeline/ApiPermissionAttribute.cs
rename to src/Squidex.Web/ApiPermissionAttribute.cs
index 48cd75e62..d515c76ce 100644
--- a/src/Squidex/Pipeline/ApiPermissionAttribute.cs
+++ b/src/Squidex.Web/ApiPermissionAttribute.cs
@@ -7,7 +7,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
-using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
@@ -15,7 +14,7 @@ using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared.Identity;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter
{
@@ -28,7 +27,7 @@ namespace Squidex.Pipeline
public ApiPermissionAttribute(params string[] ids)
{
- AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme;
+ AuthenticationSchemes = "Bearer";
permissionIds = ids;
}
@@ -50,7 +49,11 @@ namespace Squidex.Pipeline
id = id.Replace($"{{{routeParam.Key}}}", routeParam.Value?.ToString());
}
- hasPermission |= set.Allows(new Permission(id));
+ if (set.Allows(new Permission(id)))
+ {
+ hasPermission = true;
+ break;
+ }
}
if (!hasPermission)
diff --git a/src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs
similarity index 98%
rename from src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs
rename to src/Squidex.Web/AssetRequestSizeLimitAttribute.cs
index 17236e81a..2cc5479c4 100644
--- a/src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs
+++ b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs
@@ -12,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Entities.Assets;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AssetRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
diff --git a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs
similarity index 97%
rename from src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs
rename to src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs
index ef14a8bb0..0ebb669f3 100644
--- a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs
+++ b/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs
@@ -13,7 +13,7 @@ using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-namespace Squidex.Pipeline.CommandMiddlewares
+namespace Squidex.Web.CommandMiddlewares
{
public class ETagCommandMiddleware : ICommandMiddleware
{
diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
similarity index 97%
rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
rename to src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
index 4baf4b814..0bff3a6c5 100644
--- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
+++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
@@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
-namespace Squidex.Pipeline.CommandMiddlewares
+namespace Squidex.Web.CommandMiddlewares
{
public class EnrichWithActorCommandMiddleware : ICommandMiddleware
{
diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
similarity index 97%
rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
rename to src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
index f41214737..06e050784 100644
--- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
+++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
@@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-namespace Squidex.Pipeline.CommandMiddlewares
+namespace Squidex.Web.CommandMiddlewares
{
public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware
{
diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
similarity index 98%
rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
rename to src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
index f2d5ae42b..672a16b74 100644
--- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
+++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
@@ -14,7 +14,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-namespace Squidex.Pipeline.CommandMiddlewares
+namespace Squidex.Web.CommandMiddlewares
{
public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware
{
diff --git a/src/Squidex/Config/Constants.cs b/src/Squidex.Web/Constants.cs
similarity index 94%
rename from src/Squidex/Config/Constants.cs
rename to src/Squidex.Web/Constants.cs
index ee9aa76e8..770c8a435 100644
--- a/src/Squidex/Config/Constants.cs
+++ b/src/Squidex.Web/Constants.cs
@@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using IdentityServer4.Models;
+using Squidex.Infrastructure;
-namespace Squidex.Config
+namespace Squidex.Web
{
public static class Constants
{
@@ -35,7 +35,7 @@ namespace Squidex.Config
public static readonly string InternalClientId = "squidex-internal";
- public static readonly string InternalClientSecret = "squidex-internal".Sha256();
+ public static readonly string InternalClientSecret = "squidex-internal".Sha256Base64();
public static readonly string IdentityServerPrefix = "/identity-server";
}
diff --git a/src/Squidex/Pipeline/ETagExtensions.cs b/src/Squidex.Web/ETagExtensions.cs
similarity index 98%
rename from src/Squidex/Pipeline/ETagExtensions.cs
rename to src/Squidex.Web/ETagExtensions.cs
index d9f1d726c..5ee961a9d 100644
--- a/src/Squidex/Pipeline/ETagExtensions.cs
+++ b/src/Squidex.Web/ETagExtensions.cs
@@ -11,7 +11,7 @@ using System.Text;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public static class ETagExtensions
{
diff --git a/src/Squidex/Pipeline/ETagFilter.cs b/src/Squidex.Web/ETagFilter.cs
similarity index 98%
rename from src/Squidex/Pipeline/ETagFilter.cs
rename to src/Squidex.Web/ETagFilter.cs
index d14561734..b76772ad3 100644
--- a/src/Squidex/Pipeline/ETagFilter.cs
+++ b/src/Squidex.Web/ETagFilter.cs
@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ETagFilter : IAsyncActionFilter
{
diff --git a/src/Squidex/Pipeline/ETagOptions.cs b/src/Squidex.Web/ETagOptions.cs
similarity index 94%
rename from src/Squidex/Pipeline/ETagOptions.cs
rename to src/Squidex.Web/ETagOptions.cs
index c54288e77..8e832dbca 100644
--- a/src/Squidex/Pipeline/ETagOptions.cs
+++ b/src/Squidex.Web/ETagOptions.cs
@@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ETagOptions
{
diff --git a/src/Squidex/Pipeline/EntityCreatedDto.cs b/src/Squidex.Web/EntityCreatedDto.cs
similarity index 80%
rename from src/Squidex/Pipeline/EntityCreatedDto.cs
rename to src/Squidex.Web/EntityCreatedDto.cs
index 9b768e652..95738823d 100644
--- a/src/Squidex/Pipeline/EntityCreatedDto.cs
+++ b/src/Squidex.Web/EntityCreatedDto.cs
@@ -8,19 +8,15 @@
using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure.Commands;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class EntityCreatedDto
{
- ///
- /// Id of the created entity.
- ///
[Required]
+ [Display(Description = "Id of the created entity.")]
public string Id { get; set; }
-
- ///
- /// The new version of the entity.
- ///
+
+ [Display(Description = "The new version of the entity.")]
public long Version { get; set; }
public static EntityCreatedDto FromResult(EntityCreatedResult result)
diff --git a/src/Squidex/Pipeline/ErrorDto.cs b/src/Squidex.Web/ErrorDto.cs
similarity index 69%
rename from src/Squidex/Pipeline/ErrorDto.cs
rename to src/Squidex.Web/ErrorDto.cs
index 535a9e602..2d3e8f6be 100644
--- a/src/Squidex/Pipeline/ErrorDto.cs
+++ b/src/Squidex.Web/ErrorDto.cs
@@ -7,24 +7,18 @@
using System.ComponentModel.DataAnnotations;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class ErrorDto
{
- ///
- /// Error message.
- ///
[Required]
+ [Display(Description = "Error message.")]
public string Message { get; set; }
-
- ///
- /// Detailed error messages.
- ///
+
+ [Display(Description = "Detailed error messages.")]
public string[] Details { get; set; }
-
- ///
- /// Status code of the http response.
- ///
+
+ [Display(Description = "Status code of the http response.")]
public int? StatusCode { get; set; } = 400;
}
}
diff --git a/src/Squidex/Pipeline/Extensions.cs b/src/Squidex.Web/Extensions.cs
similarity index 96%
rename from src/Squidex/Pipeline/Extensions.cs
rename to src/Squidex.Web/Extensions.cs
index 98f693727..b7f7594bf 100644
--- a/src/Squidex/Pipeline/Extensions.cs
+++ b/src/Squidex.Web/Extensions.cs
@@ -6,10 +6,9 @@
// ==========================================================================
using System.Security.Claims;
-using Squidex.Config;
using Squidex.Infrastructure.Security;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public static class Extensions
{
diff --git a/src/Squidex/Pipeline/FileCallbackResult.cs b/src/Squidex.Web/FileCallbackResult.cs
similarity index 93%
rename from src/Squidex/Pipeline/FileCallbackResult.cs
rename to src/Squidex.Web/FileCallbackResult.cs
index 5cc764a99..5ca752eea 100644
--- a/src/Squidex/Pipeline/FileCallbackResult.cs
+++ b/src/Squidex.Web/FileCallbackResult.cs
@@ -10,8 +10,9 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
+using Squidex.Web.Pipeline;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public sealed class FileCallbackResult : FileResult
{
diff --git a/src/Squidex/Pipeline/IApiCostsFeature.cs b/src/Squidex.Web/IApiCostsFeature.cs
similarity index 94%
rename from src/Squidex/Pipeline/IApiCostsFeature.cs
rename to src/Squidex.Web/IApiCostsFeature.cs
index f4cfa1b97..9f1e380f5 100644
--- a/src/Squidex/Pipeline/IApiCostsFeature.cs
+++ b/src/Squidex.Web/IApiCostsFeature.cs
@@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public interface IApiCostsFeature
{
diff --git a/src/Squidex/Pipeline/IAppFeature.cs b/src/Squidex.Web/IAppFeature.cs
similarity index 94%
rename from src/Squidex/Pipeline/IAppFeature.cs
rename to src/Squidex.Web/IAppFeature.cs
index d8a23b925..a798da598 100644
--- a/src/Squidex/Pipeline/IAppFeature.cs
+++ b/src/Squidex.Web/IAppFeature.cs
@@ -7,7 +7,7 @@
using Squidex.Domain.Apps.Entities.Apps;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public interface IAppFeature
{
diff --git a/src/Squidex/Pipeline/IGenerateEtag.cs b/src/Squidex.Web/IGenerateEtag.cs
similarity index 94%
rename from src/Squidex/Pipeline/IGenerateEtag.cs
rename to src/Squidex.Web/IGenerateEtag.cs
index e1f0c8789..6986f1acc 100644
--- a/src/Squidex/Pipeline/IGenerateEtag.cs
+++ b/src/Squidex.Web/IGenerateEtag.cs
@@ -7,7 +7,7 @@
using System;
-namespace Squidex.Pipeline
+namespace Squidex.Web
{
public interface IGenerateETag
{
diff --git a/src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs b/src/Squidex.Web/MyJsonInheritanceConverter.cs
similarity index 98%
rename from src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs
rename to src/Squidex.Web/MyJsonInheritanceConverter.cs
index ed16e20b3..ff3a1854e 100644
--- a/src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs
+++ b/src/Squidex.Web/MyJsonInheritanceConverter.cs
@@ -16,7 +16,7 @@ using Squidex.Infrastructure;
#pragma warning disable RECS0108 // Warns about static fields in generic types
-namespace Squidex.Areas.Api.Controllers
+namespace Squidex.Web
{
public class MyJsonInheritanceConverter : JsonInheritanceConverter
{
diff --git a/src/Squidex/Pipeline/ActionContextLogAppender.cs b/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs
similarity index 93%
rename from src/Squidex/Pipeline/ActionContextLogAppender.cs
rename to src/Squidex.Web/Pipeline/ActionContextLogAppender.cs
index 97c6060b3..8db6973d1 100644
--- a/src/Squidex/Pipeline/ActionContextLogAppender.cs
+++ b/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure.Log;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class ActionContextLogAppender : ILogAppender
{
@@ -57,9 +57,9 @@ namespace Squidex.Pipeline
{
w.WriteObject("routeValues", actionContext.ActionDescriptor.RouteValues, (routeValues, r) =>
{
- foreach (var (key, val) in routeValues)
+ foreach (var kvp in routeValues)
{
- r.WriteProperty(key, val);
+ r.WriteProperty(kvp.Key, kvp.Value);
}
});
}
diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs
similarity index 98%
rename from src/Squidex/Pipeline/ApiCostsFilter.cs
rename to src/Squidex.Web/Pipeline/ApiCostsFilter.cs
index 2dc93f326..0859a26c3 100644
--- a/src/Squidex/Pipeline/ApiCostsFilter.cs
+++ b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs
@@ -15,7 +15,7 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.UsageTracking;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer
{
diff --git a/src/Squidex/Pipeline/ApiPermissionUnifier.cs b/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs
similarity index 90%
rename from src/Squidex/Pipeline/ApiPermissionUnifier.cs
rename to src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs
index 98be130af..672022d73 100644
--- a/src/Squidex/Pipeline/ApiPermissionUnifier.cs
+++ b/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs
@@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@@ -12,7 +13,7 @@ using Microsoft.AspNetCore.Authentication;
using Squidex.Shared;
using Squidex.Shared.Identity;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class ApiPermissionUnifier : IClaimsTransformation
{
@@ -22,7 +23,7 @@ namespace Squidex.Pipeline
{
var identity = principal.Identities.First();
- if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole))
+ if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole, StringComparison.OrdinalIgnoreCase))
{
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin));
}
diff --git a/src/Squidex/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs
similarity index 90%
rename from src/Squidex/Pipeline/AppResolver.cs
rename to src/Squidex.Web/Pipeline/AppResolver.cs
index 705bfae79..cc0ae853b 100644
--- a/src/Squidex/Pipeline/AppResolver.cs
+++ b/src/Squidex.Web/Pipeline/AppResolver.cs
@@ -17,7 +17,7 @@ using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class AppResolver : IAsyncActionFilter
{
@@ -61,17 +61,6 @@ namespace Squidex.Pipeline
(role, permissions) = FindByOpenIdClient(app, user);
}
- if (permissions == null || permissions.Count == 0)
- {
- var set = user.Permissions();
-
- if (!set.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context))
- {
- context.Result = new NotFoundResult();
- return;
- }
- }
-
if (permissions != null)
{
var identity = user.Identities.First();
@@ -84,6 +73,14 @@ namespace Squidex.Pipeline
}
}
+ var set = user.Permissions();
+
+ if (!set.Includes(Permissions.ForApp(Permissions.App, appName))&& !AllowAnonymous(context))
+ {
+ context.Result = new NotFoundResult();
+ return;
+ }
+
context.HttpContext.Features.Set(new AppFeature(app));
}
diff --git a/src/Squidex/Pipeline/CleanupHostMiddleware.cs b/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs
similarity index 97%
rename from src/Squidex/Pipeline/CleanupHostMiddleware.cs
rename to src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs
index 504a5a455..82c98ad21 100644
--- a/src/Squidex/Pipeline/CleanupHostMiddleware.cs
+++ b/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs
@@ -8,7 +8,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public class CleanupHostMiddleware
{
diff --git a/src/Squidex/Pipeline/EnforceHttpsMiddleware.cs b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs
similarity index 88%
rename from src/Squidex/Pipeline/EnforceHttpsMiddleware.cs
rename to src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs
index 4f4c6158e..499b683f2 100644
--- a/src/Squidex/Pipeline/EnforceHttpsMiddleware.cs
+++ b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs
@@ -9,15 +9,14 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
-using Squidex.Config;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class EnforceHttpsMiddleware : IMiddleware
{
- private readonly IOptions urls;
+ private readonly IOptions urls;
- public EnforceHttpsMiddleware(IOptions urls)
+ public EnforceHttpsMiddleware(IOptions urls)
{
this.urls = urls;
}
diff --git a/src/Squidex/Pipeline/FileCallbackResultExecutor.cs b/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs
similarity index 97%
rename from src/Squidex/Pipeline/FileCallbackResultExecutor.cs
rename to src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs
index 5033af37f..fc8b011d7 100644
--- a/src/Squidex/Pipeline/FileCallbackResultExecutor.cs
+++ b/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
diff --git a/src/Squidex/Pipeline/LocalCacheMiddleware.cs b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
similarity index 97%
rename from src/Squidex/Pipeline/LocalCacheMiddleware.cs
rename to src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
index 064e6a520..b9f050191 100644
--- a/src/Squidex/Pipeline/LocalCacheMiddleware.cs
+++ b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class LocalCacheMiddleware : IMiddleware
{
diff --git a/src/Squidex/Pipeline/MeasureResultFilter.cs b/src/Squidex.Web/Pipeline/MeasureResultFilter.cs
similarity index 97%
rename from src/Squidex/Pipeline/MeasureResultFilter.cs
rename to src/Squidex.Web/Pipeline/MeasureResultFilter.cs
index 2d22afd65..de2220f32 100644
--- a/src/Squidex/Pipeline/MeasureResultFilter.cs
+++ b/src/Squidex.Web/Pipeline/MeasureResultFilter.cs
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure.Log;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class MeasureResultFilter : IAsyncResultFilter, IAsyncActionFilter
{
diff --git a/src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs b/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
similarity index 98%
rename from src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs
rename to src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
index 87915019a..c2e3c7006 100644
--- a/src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs
+++ b/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs
@@ -11,7 +11,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Pipeline
{
public sealed class RequestLogPerformanceMiddleware : IMiddleware
{
diff --git a/src/Squidex/Pipeline/UrlGenerator.cs b/src/Squidex.Web/Services/UrlGenerator.cs
similarity index 92%
rename from src/Squidex/Pipeline/UrlGenerator.cs
rename to src/Squidex.Web/Services/UrlGenerator.cs
index d2c1266e7..ef2bab32d 100644
--- a/src/Squidex/Pipeline/UrlGenerator.cs
+++ b/src/Squidex.Web/Services/UrlGenerator.cs
@@ -7,7 +7,6 @@
using System;
using Microsoft.Extensions.Options;
-using Squidex.Config;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Entities.Apps;
@@ -18,16 +17,16 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
-namespace Squidex.Pipeline
+namespace Squidex.Web.Services
{
public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator
{
private readonly IAssetStore assetStore;
- private readonly MyUrlsOptions urlsOptions;
+ private readonly UrlsOptions urlsOptions;
public bool CanGenerateAssetSourceUrl { get; }
- public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl)
+ public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl)
{
this.assetStore = assetStore;
this.urlsOptions = urlsOptions.Value;
diff --git a/src/Squidex.Web/Squidex.Web.csproj b/src/Squidex.Web/Squidex.Web.csproj
new file mode 100644
index 000000000..bbe645f13
--- /dev/null
+++ b/src/Squidex.Web/Squidex.Web.csproj
@@ -0,0 +1,23 @@
+
+
+ netstandard2.0
+ 7.3
+
+
+ full
+ True
+
+
+
+
+
+
+
+
+
+ ..\..\Squidex.ruleset
+
+
+
+
+
diff --git a/src/Squidex.Web/SquidexWeb.cs b/src/Squidex.Web/SquidexWeb.cs
new file mode 100644
index 000000000..ea1ea613b
--- /dev/null
+++ b/src/Squidex.Web/SquidexWeb.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Reflection;
+
+#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static.
+
+namespace Squidex.Web
+{
+ public sealed class SquidexWeb
+ {
+ public static readonly Assembly Assembly = typeof(SquidexWeb).Assembly;
+ }
+}
diff --git a/src/Squidex/Config/MyUrlsOptions.cs b/src/Squidex.Web/UrlsOptions.cs
similarity index 93%
rename from src/Squidex/Config/MyUrlsOptions.cs
rename to src/Squidex.Web/UrlsOptions.cs
index 32e4022c0..b45390301 100644
--- a/src/Squidex/Config/MyUrlsOptions.cs
+++ b/src/Squidex.Web/UrlsOptions.cs
@@ -7,9 +7,9 @@
using Squidex.Infrastructure;
-namespace Squidex.Config
+namespace Squidex.Web
{
- public sealed class MyUrlsOptions
+ public sealed class UrlsOptions
{
public bool EnforceHTTPS { get; set; }
diff --git a/src/Squidex/Config/MyUsageOptions.cs b/src/Squidex.Web/UsageOptions.cs
similarity index 89%
rename from src/Squidex/Config/MyUsageOptions.cs
rename to src/Squidex.Web/UsageOptions.cs
index 339c665bc..c9502e38e 100644
--- a/src/Squidex/Config/MyUsageOptions.cs
+++ b/src/Squidex.Web/UsageOptions.cs
@@ -7,9 +7,9 @@
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations;
-namespace Squidex.Config
+namespace Squidex.Web
{
- public class MyUsageOptions
+ public sealed class UsageOptions
{
public ConfigAppLimitsPlan[] Plans { get; set; }
}
diff --git a/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs
index e0d01097e..9bd4f0b70 100644
--- a/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs
+++ b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs
@@ -13,9 +13,8 @@ using Microsoft.AspNetCore.Authorization;
using NSwag;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
-using Squidex.Config;
using Squidex.Infrastructure.Tasks;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Config.Swagger
{
diff --git a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
index 9133a2e86..2b5b5b24c 100644
--- a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
+++ b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs
@@ -9,19 +9,19 @@ using System.Collections.Generic;
using Microsoft.Extensions.Options;
using NSwag;
using NSwag.SwaggerGeneration.Processors.Security;
-using Squidex.Config;
using Squidex.Pipeline.Swagger;
+using Squidex.Web;
namespace Squidex.Areas.Api.Config.Swagger
{
public class SecurityProcessor : SecurityDefinitionAppender
{
- public SecurityProcessor(IOptions urlOptions)
+ public SecurityProcessor(IOptions urlOptions)
: base(Constants.SecurityDefinition, CreateOAuthSchema(urlOptions.Value))
{
}
- private static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions)
+ private static SwaggerSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions)
{
var securityScheme = new SwaggerSecurityScheme();
diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
index 5059cb21c..ddccd616c 100644
--- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
+++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs
@@ -14,7 +14,6 @@ using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.Rules.Models;
-using Squidex.Config;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Config.Swagger
diff --git a/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs
index b69e54972..bc5106af9 100644
--- a/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs
+++ b/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs
@@ -10,8 +10,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
-using Squidex.Config;
using Squidex.Infrastructure.Tasks;
+using Squidex.Web;
namespace Squidex.Areas.Api.Config.Swagger
{
@@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Config.Swagger
private readonly string url;
- public ThemeProcessor(IOptions urlOptions)
+ public ThemeProcessor(IOptions urlOptions)
{
url = urlOptions.Value.BuildUrl("images/logo-white.png", false);
}
diff --git a/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
index 01dc1c73b..b0ecdf36d 100644
--- a/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
+++ b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs
@@ -22,7 +22,7 @@ namespace Squidex.Areas.Api.Config.Swagger
{
foreach (var controllerType in context.ControllerTypes)
{
- var attribute = controllerType.GetTypeInfo().GetCustomAttribute();
+ var attribute = controllerType.GetCustomAttribute();
if (attribute != null)
{
diff --git a/src/Squidex/Areas/Api/Controllers/ApiController.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs
index e991604c3..03ccc89c1 100644
--- a/src/Squidex/Areas/Api/Controllers/ApiController.cs
+++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
index 085170779..f60427864 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
@@ -12,8 +12,8 @@ using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
index 571d9be66..d8af9f53b 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
@@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
index 4165652f7..17ce8c320 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
@@ -13,8 +13,8 @@ using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
index a31267774..29783a09e 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
@@ -13,8 +13,8 @@ using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
index a8aacc1ba..e146c90b3 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
@@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
index fb765658c..4219f42dd 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
@@ -16,9 +16,9 @@ using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
-using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
index e2245fb41..7fe96d7bc 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
@@ -14,8 +14,8 @@ using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
index 9d7b7ce3e..cf71326c0 100644
--- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
@@ -15,7 +15,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
-using Squidex.Pipeline;
+using Squidex.Web;
#pragma warning disable 1573
diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
index d467d6543..0d00940d3 100644
--- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
@@ -23,8 +23,8 @@ using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets
{
diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
index e0ce409c5..733a3ab18 100644
--- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
@@ -12,7 +12,7 @@ using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
index ba2dbe410..25c965033 100644
--- a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups
{
diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
index 3c433fbdb..0198fa186 100644
--- a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
@@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups
{
diff --git a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
index d564e8d11..6ad1cf1c0 100644
--- a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
@@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Backups
{
diff --git a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
index 281c63f67..5603ec207 100644
--- a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
@@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Comments
{
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
index aaf2aadf3..c06a178c5 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents
{
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
index df849dd56..f5467dfad 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
@@ -20,9 +20,9 @@ using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents
{
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
index 6f5812945..7809731b0 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
@@ -10,13 +10,13 @@ using System.Collections.Generic;
using System.Linq;
using NJsonSchema;
using NSwag;
-using Squidex.Config;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
index 0ab353afb..e2e7a8d2c 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
@@ -18,23 +18,23 @@ using NSwag.SwaggerGeneration;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Areas.Api.Config.Swagger;
-using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
public sealed class SchemasSwaggerGenerator
{
- private readonly MyUrlsOptions urlOptions;
+ private readonly UrlsOptions urlOptions;
private readonly SwaggerDocumentSettings settings = new SwaggerDocumentSettings();
private SwaggerJsonSchemaGenerator schemaGenerator;
private SwaggerDocument document;
private JsonSchemaResolver schemaResolver;
- public SchemasSwaggerGenerator(IOptions urlOptions, IEnumerable documentProcessors)
+ public SchemasSwaggerGenerator(IOptions urlOptions, IEnumerable documentProcessors)
{
this.urlOptions = urlOptions.Value;
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
index 12fdd6cb4..5e896298b 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
@@ -16,7 +16,7 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
index f8a2c9d0d..1369eb9c7 100644
--- a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
+++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
@@ -13,8 +13,8 @@ using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.EventConsumers
{
diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
index e09a2aa94..bb5b5fc57 100644
--- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
+++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
@@ -11,8 +11,8 @@ using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.History
{
diff --git a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
index 77ca6fed2..85f5c5133 100644
--- a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Languages
{
diff --git a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs
index e80dc19b7..f163227f0 100644
--- a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.News.Models;
using Squidex.Areas.Api.Controllers.News.Service;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.News
{
diff --git a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
index c27e14477..22a0254ad 100644
--- a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
@@ -7,8 +7,8 @@
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Ping
{
diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
index 5b5a8fe65..9490a4f48 100644
--- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
@@ -11,8 +11,8 @@ using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Plans.Models;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Plans
{
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
index 2b9bd6328..e0d44e0e4 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs
@@ -33,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters
public RuleTriggerDto Visit(SchemaChangedTrigger trigger)
{
- return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto());
+ return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto());
}
public RuleTriggerDto Visit(UsageTrigger trigger)
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs
index 4efcfb7ad..8f1da7b9e 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs
@@ -7,15 +7,14 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Extensions.Actions;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleActionConverter : MyJsonInheritanceConverter
{
- private static readonly Dictionary Mapping = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => x.Value.Type);
+ public static IReadOnlyDictionary Mapping { get; set; }
public RuleActionConverter()
: base("actionType", Mapping)
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs
index a0d23e12b..b0337ed42 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs
@@ -11,13 +11,23 @@ using System.Threading.Tasks;
using NJsonSchema;
using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
-using Squidex.Extensions.Actions;
+using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleActionProcessor : IDocumentProcessor
{
+ private readonly RuleRegistry ruleRegistry;
+
+ public RuleActionProcessor(RuleRegistry ruleRegistry)
+ {
+ Guard.NotNull(ruleRegistry, nameof(ruleRegistry));
+
+ this.ruleRegistry = ruleRegistry;
+ }
+
public async Task ProcessAsync(DocumentProcessorContext context)
{
try
@@ -36,16 +46,16 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
Type = JsonObjectType.String, IsRequired = true
};
- foreach (var derived in RuleElementRegistry.Actions)
+ foreach (var action in ruleRegistry.Actions)
{
- var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value.Type, context.SchemaResolver);
+ var derivedSchema = await context.SchemaGenerator.GenerateAsync(action.Value.Type, context.SchemaResolver);
var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key;
if (oldName != null)
{
context.Document.Definitions.Remove(oldName);
- context.Document.Definitions.Add(derived.Key, derivedSchema);
+ context.Document.Definitions.Add(action.Key, derivedSchema);
}
}
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
index a782c9e2d..4ee256530 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
@@ -14,7 +14,7 @@ using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs
index e419be526..910e87c38 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs
@@ -6,6 +6,9 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
@@ -37,5 +40,20 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models
/// The optional link to the product that is integrated.
///
public string ReadMore { get; set; }
+
+ ///
+ /// The properties.
+ ///
+ [Required]
+ public RuleElementPropertyDto[] Properties { get; set; }
+
+ public static RuleElementDto FromDefinition(RuleActionDefinition definition)
+ {
+ var result = SimpleMapper.Map(definition, new RuleElementDto());
+
+ result.Properties = definition.Properties.Select(x => SimpleMapper.Map(x, new RuleElementPropertyDto())).ToArray();
+
+ return result;
+ }
}
}
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs
new file mode 100644
index 000000000..ea9c72a64
--- /dev/null
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs
@@ -0,0 +1,49 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
+using Squidex.Domain.Apps.Core.HandleRules;
+
+namespace Squidex.Areas.Api.Controllers.Rules.Models
+{
+ public sealed class RuleElementPropertyDto
+ {
+ ///
+ /// The html editor.
+ ///
+ [Required]
+ public RuleActionPropertyEditor Editor { get; set; }
+
+ ///
+ /// The name of the editor.
+ ///
+ [Required]
+ public string Name { get; set; }
+
+ ///
+ /// The label to use.
+ ///
+ [Required]
+ public string Display { get; set; }
+
+ ///
+ /// The optional description.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Indicates if the property is formattable.
+ ///
+ public bool IsFormattable { get; set; }
+
+ ///
+ /// Indicates if the property is required.
+ ///
+ public bool IsRequired { get; set; }
+ }
+}
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs
index 7671db829..9ac6cd699 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
index 998f08a70..b657ef1cf 100644
--- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
@@ -13,15 +13,14 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models;
+using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
-using Squidex.Extensions.Actions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Infrastructure.Reflection;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Rules
{
@@ -31,18 +30,18 @@ namespace Squidex.Areas.Api.Controllers.Rules
[ApiExplorerSettings(GroupName = nameof(Rules))]
public sealed class RulesController : ApiController
{
- private static readonly string RuleActionsEtag = string.Join(";", RuleElementRegistry.Actions.Select(x => x.Key)).Sha256Base64();
- private static readonly string RuleTriggersEtag = string.Join(";", RuleElementRegistry.Triggers.Select(x => x.Key)).Sha256Base64();
private readonly IAppProvider appProvider;
private readonly IRuleEventRepository ruleEventsRepository;
+ private readonly RuleRegistry ruleRegistry;
public RulesController(ICommandBus commandBus, IAppProvider appProvider,
- IRuleEventRepository ruleEventsRepository)
+ IRuleEventRepository ruleEventsRepository, RuleRegistry ruleRegistry)
: base(commandBus)
{
this.appProvider = appProvider;
this.ruleEventsRepository = ruleEventsRepository;
+ this.ruleRegistry = ruleRegistry;
}
///
@@ -58,29 +57,11 @@ namespace Squidex.Areas.Api.Controllers.Rules
[ApiCosts(0)]
public IActionResult GetActions()
{
- var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
+ var etag = string.Join(";", ruleRegistry.Actions.Select(x => x.Key)).Sha256Base64();
- Response.Headers[HeaderNames.ETag] = RuleActionsEtag;
+ var response = ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDefinition(x.Value));
- return Ok(response);
- }
-
- ///
- /// Get the supported rule triggers.
- ///
- ///
- /// 200 => Rule triggers returned.
- ///
- [HttpGet]
- [Route("rules/triggers/")]
- [ProducesResponseType(typeof(Dictionary), 200)]
- [ApiPermission]
- [ApiCosts(0)]
- public IActionResult GetTriggers()
- {
- var response = RuleElementRegistry.Triggers.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
-
- Response.Headers[HeaderNames.ETag] = RuleTriggersEtag;
+ Response.Headers[HeaderNames.ETag] = etag;
return Ok(response);
}
diff --git a/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs b/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
deleted file mode 100644
index 54e9e1566..000000000
--- a/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System.Threading.Tasks;
-using CoreTweet;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
-using Squidex.Extensions.Actions.Twitter;
-
-namespace Squidex.Areas.Api.Controllers.Rules
-{
- public sealed class TwitterController : Controller
- {
- private readonly TwitterOptions twitterOptions;
-
- public TwitterController(IOptions twitterOptions)
- {
- this.twitterOptions = twitterOptions.Value;
- }
-
- public sealed class TokenRequest
- {
- public string PinCode { get; set; }
-
- public string RequestToken { get; set; }
-
- public string RequestTokenSecret { get; set; }
- }
-
- [HttpGet]
- [Route("rules/twitter/auth")]
- public async Task Auth()
- {
- var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret);
-
- return Ok(new
- {
- session.AuthorizeUri,
- session.RequestToken,
- session.RequestTokenSecret
- });
- }
-
- [HttpPost]
- [Route("rules/twitter/token")]
- public async Task AuthComplete([FromBody] TokenRequest request)
- {
- var session = new OAuth.OAuthSession
- {
- ConsumerKey = twitterOptions.ClientId,
- ConsumerSecret = twitterOptions.ClientSecret,
- RequestToken = request.RequestToken,
- RequestTokenSecret = request.RequestTokenSecret
- };
-
- var tokens = await session.GetTokensAsync(request.PinCode);
-
- return Ok(new
- {
- tokens.AccessToken,
- tokens.AccessTokenSecret
- });
- }
- }
-}
diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
index afb21c6b9..02376143b 100644
--- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.Schemas;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
index 73794552f..f66d64d23 100644
--- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
@@ -11,7 +11,7 @@ using NodaTime;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
index 048607c97..74a36f916 100644
--- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
@@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas
{
diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
index 777495ffd..1fb64ec83 100644
--- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
@@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas
{
diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
index 86287b9b0..d7a7f4eaa 100644
--- a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
@@ -13,14 +13,13 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Areas.Api.Controllers.Statistics.Models;
-using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Statistics
{
@@ -35,7 +34,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
private readonly IAppPlansProvider appPlansProvider;
private readonly IAssetUsageTracker assetStatsRepository;
private readonly IDataProtector dataProtector;
- private readonly MyUrlsOptions urlsOptions;
+ private readonly UrlsOptions urlsOptions;
public UsagesController(
ICommandBus commandBus,
@@ -44,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
IAppPlansProvider appPlansProvider,
IAssetUsageTracker assetStatsRepository,
IDataProtectionProvider dataProtection,
- IOptions urlsOptions)
+ IOptions urlsOptions)
: base(commandBus)
{
this.usageTracker = usageTracker;
diff --git a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
index e5fbe040e..5fc5cd8e4 100644
--- a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
@@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Translations
{
diff --git a/src/Squidex/Config/MyUIOptions.cs b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
similarity index 95%
rename from src/Squidex/Config/MyUIOptions.cs
rename to src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
index 5ac51ece4..5d87d6df8 100644
--- a/src/Squidex/Config/MyUIOptions.cs
+++ b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
@@ -7,7 +7,7 @@
using System.Collections.Generic;
-namespace Squidex.Config
+namespace Squidex.Areas.Api.Controllers.UI
{
public sealed class MyUIOptions
{
diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs
index 18a7eeb7e..1de619114 100644
--- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs
+++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs
@@ -10,30 +10,26 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models;
-using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
-using Squidex.Extensions.Actions.Twitter;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
-using Squidex.Pipeline;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.UI
{
public sealed class UIController : ApiController
{
private readonly MyUIOptions uiOptions;
- private readonly TwitterOptions twitterOptions;
private readonly IGrainFactory grainFactory;
public UIController(ICommandBus commandBus,
IOptions uiOptions,
- IOptions twitterOptions,
IGrainFactory grainFactory)
: base(commandBus)
{
this.uiOptions = uiOptions.Value;
+
this.grainFactory = grainFactory;
- this.twitterOptions = twitterOptions.Value;
}
///
@@ -55,8 +51,6 @@ namespace Squidex.Areas.Api.Controllers.UI
result.Value.Add("mapType", uiOptions.Map?.Type ?? "OSM");
result.Value.Add("mapKey", uiOptions.Map?.GoogleMaps?.Key);
- result.Value.Add("supportTwitterAction", twitterOptions.IsConfigured());
-
return Ok(result.Value);
}
diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
index ef7e8cb65..7d547be14 100644
--- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
@@ -15,8 +15,8 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
-using Squidex.Pipeline;
using Squidex.Shared;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users
{
diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
index 3aef01d84..406510caf 100644
--- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
@@ -9,15 +9,14 @@ using System;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
-using Squidex.Pipeline;
using Squidex.Shared.Users;
+using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users
{
@@ -34,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Users
static UsersController()
{
- var assembly = typeof(UsersController).GetTypeInfo().Assembly;
+ var assembly = typeof(UsersController).Assembly;
using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png"))
{
diff --git a/src/Squidex/Areas/Api/Startup.cs b/src/Squidex/Areas/Api/Startup.cs
index 5d2cefaa6..55e2383fc 100644
--- a/src/Squidex/Areas/Api/Startup.cs
+++ b/src/Squidex/Areas/Api/Startup.cs
@@ -7,7 +7,7 @@
using Microsoft.AspNetCore.Builder;
using Squidex.Areas.Api.Config.Swagger;
-using Squidex.Config;
+using Squidex.Web;
namespace Squidex.Areas.Api
{
diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
index f5025efd6..8ede8a1bb 100644
--- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
+++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
@@ -6,7 +6,6 @@
// ==========================================================================
using System.Collections.Generic;
-using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityModel;
using IdentityServer4.Models;
@@ -17,9 +16,9 @@ using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
-using Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Shared.Identity;
+using Squidex.Web;
namespace Squidex.Areas.IdentityServer.Config
{
@@ -29,7 +28,7 @@ namespace Squidex.Areas.IdentityServer.Config
{
X509Certificate2 certificate;
- var assembly = typeof(IdentityServerServices).GetTypeInfo().Assembly;
+ var assembly = typeof(IdentityServerServices).Assembly;
using (var certStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx"))
{
diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
index af60dc5e3..c4b619466 100644
--- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
+++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
@@ -17,9 +17,9 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
-using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
+using Squidex.Web;
namespace Squidex.Areas.IdentityServer.Config
{
@@ -29,7 +29,7 @@ namespace Squidex.Areas.IdentityServer.Config
private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase);
public LazyClientStore(
- IOptions urlsOptions,
+ IOptions urlsOptions,
IOptions identityOptions,
IAppProvider appProvider)
{
@@ -89,7 +89,7 @@ namespace Squidex.Areas.IdentityServer.Config
};
}
- private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions)
+ private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions)
{
foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value))
{
@@ -97,7 +97,7 @@ namespace Squidex.Areas.IdentityServer.Config
}
}
- private static IEnumerable CreateStaticClients(MyUrlsOptions urlsOptions, MyIdentityOptions identityOptions)
+ private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions)
{
var frontendId = Constants.FrontendClient;
diff --git a/src/Squidex/Areas/IdentityServer/Startup.cs b/src/Squidex/Areas/IdentityServer/Startup.cs
index e98f0aff3..d46ab509e 100644
--- a/src/Squidex/Areas/IdentityServer/Startup.cs
+++ b/src/Squidex/Areas/IdentityServer/Startup.cs
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Areas.IdentityServer.Config;
-using Squidex.Config;
+using Squidex.Web;
namespace Squidex.Areas.IdentityServer
{
diff --git a/src/Squidex/Areas/OrleansDashboard/Startup.cs b/src/Squidex/Areas/OrleansDashboard/Startup.cs
index d8b5da6f8..943057450 100644
--- a/src/Squidex/Areas/OrleansDashboard/Startup.cs
+++ b/src/Squidex/Areas/OrleansDashboard/Startup.cs
@@ -8,7 +8,7 @@
using Microsoft.AspNetCore.Builder;
using Orleans;
using Squidex.Areas.OrleansDashboard.Middlewares;
-using Squidex.Config;
+using Squidex.Web;
namespace Squidex.Areas.OrleansDashboard
{
diff --git a/src/Squidex/Areas/Portal/Startup.cs b/src/Squidex/Areas/Portal/Startup.cs
index 70490676a..88cc7646b 100644
--- a/src/Squidex/Areas/Portal/Startup.cs
+++ b/src/Squidex/Areas/Portal/Startup.cs
@@ -7,7 +7,7 @@
using Microsoft.AspNetCore.Builder;
using Squidex.Areas.Portal.Middlewares;
-using Squidex.Config;
+using Squidex.Web;
namespace Squidex.Areas.Portal
{
diff --git a/src/Squidex/Config/Authentication/IdentityServerServices.cs b/src/Squidex/Config/Authentication/IdentityServerServices.cs
index bcf0b9916..deb1a9ad1 100644
--- a/src/Squidex/Config/Authentication/IdentityServerServices.cs
+++ b/src/Squidex/Config/Authentication/IdentityServerServices.cs
@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
+using Squidex.Web;
namespace Squidex.Config.Authentication
{
@@ -20,7 +21,7 @@ namespace Squidex.Config.Authentication
{
var apiScope = Constants.ApiScope;
- var urlsOptions = config.GetSection("urls").Get();
+ var urlsOptions = config.GetSection("urls").Get();
if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl))
{
diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs
index 5d34ee58d..de3eebe6a 100644
--- a/src/Squidex/Config/Authentication/OidcServices.cs
+++ b/src/Squidex/Config/Authentication/OidcServices.cs
@@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
+using Squidex.Web;
namespace Squidex.Config.Authentication
{
diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs
index 8ec712b17..669cd29b8 100644
--- a/src/Squidex/Config/Domain/AssetServices.cs
+++ b/src/Squidex/Config/Domain/AssetServices.cs
@@ -20,21 +20,26 @@ namespace Squidex.Config.Domain
{
public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config)
{
- config.ConfigureByOption("assetStore:type", new Options
+ config.ConfigureByOption("assetStore:type", new Alternatives
{
+ ["Default"] = () =>
+ {
+ services.AddSingletonAs()
+ .AsOptional();
+ },
["Folder"] = () =>
{
var path = config.GetRequiredValue("assetStore:folder:path");
services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService()))
- .As();
+ .AsOptional();
},
["GoogleCloud"] = () =>
{
var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket");
services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName))
- .As();
+ .AsOptional();
},
["AzureBlob"] = () =>
{
@@ -42,7 +47,7 @@ namespace Squidex.Config.Domain
var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName");
services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName))
- .As();
+ .AsOptional();
},
["MongoDb"] = () =>
{
@@ -62,7 +67,7 @@ namespace Squidex.Config.Domain
return new MongoGridFsAssetStore(gridFsbucket);
})
- .As();
+ .AsOptional();
}
});
diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs
index 8a8b554a7..b1deb7ae3 100644
--- a/src/Squidex/Config/Domain/EntitiesServices.cs
+++ b/src/Squidex/Config/Domain/EntitiesServices.cs
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Options;
using Migrate_01;
using Migrate_01.Migrations;
using Orleans;
+using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.HandleRules;
@@ -31,6 +32,7 @@ using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
+using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
@@ -44,8 +46,10 @@ using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
-using Squidex.Pipeline;
-using Squidex.Pipeline.CommandMiddlewares;
+using Squidex.Infrastructure.Orleans;
+using Squidex.Web;
+using Squidex.Web.CommandMiddlewares;
+using Squidex.Web.Services;
namespace Squidex.Config.Domain
{
@@ -56,18 +60,16 @@ namespace Squidex.Config.Domain
var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true);
services.AddSingletonAs(c => new UrlGenerator(
- c.GetRequiredService>(),
+ c.GetRequiredService>(),
c.GetRequiredService(),
exposeSourceUrl))
.As().As().As();
services.AddSingletonAs()
- .As()
- .As();
+ .As().As