diff --git a/.drone.yml b/.drone.yml index b72edbb34..389a52a4a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -21,7 +21,7 @@ pipeline: secrets: [ docker_username, docker_password ] when: event: push - branch: [master] + branch: [ master ] build_release: image: docker diff --git a/Dockerfile b/Dockerfile index c02ef4137..8135ffc5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # # Stage 1, Prebuild # -FROM microsoft/aspnetcore-build:2.0.0-jessie as builder +FROM microsoft/aspnetcore-build:2.0.3-jessie as builder # Install runtime dependencies RUN apt-get update \ @@ -45,7 +45,7 @@ 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 tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj -v d \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj \ @@ -57,7 +57,7 @@ RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Rel # # Stage 2, Build runtime # -FROM microsoft/aspnetcore:2.0.0-jessie +FROM microsoft/aspnetcore:2.0.3-jessie # Default AspNetCore directory WORKDIR /app @@ -66,5 +66,7 @@ WORKDIR /app COPY --from=builder /out/ . EXPOSE 80 +EXPOSE 33333 +EXPOSE 40000 ENTRYPOINT ["dotnet", "Squidex.dll"] \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config index 3f0e00340..c982f5720 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,6 +1,7 @@  + \ No newline at end of file diff --git a/Squidex.sln b/Squidex.sln index 81f85c0f9..21ac0f2e4 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}" EndProject @@ -34,10 +34,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Rabb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.GoogleCloud", "src\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj", "{945871B1-77B8-43FB-B53C-27CF385AB756}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B56EBCEC-9C50-46A7-848C-65502DE69C5C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{D48A03DF-BCD3-4667-8747-2F251347E2B6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "migrations", "migrations", "{94207AA6-4923-4183-A558-E0F8196B8CA3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{B51126A8-0D75-4A79-867D-10724EC6AC84}" @@ -67,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Mo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -197,18 +195,6 @@ Global {945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x64.Build.0 = Release|Any CPU {945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x86.ActiveCfg = Release|Any CPU {945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x86.Build.0 = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x64.ActiveCfg = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x64.Build.0 = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x86.ActiveCfg = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x86.Build.0 = Debug|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|Any CPU.Build.0 = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x64.ActiveCfg = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x64.Build.0 = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x86.ActiveCfg = Release|Any CPU - {D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x86.Build.0 = Release|Any CPU {B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|Any CPU.Build.0 = Debug|Any CPU {B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -317,6 +303,18 @@ Global {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.Build.0 = Release|Any CPU {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.ActiveCfg = Release|Any CPU {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.Build.0 = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x64.Build.0 = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Debug|x86.Build.0 = Debug|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|Any CPU.Build.0 = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.ActiveCfg = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x64.Build.0 = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.ActiveCfg = Release|Any CPU + {9B4A55F4-D9A4-4FC3-8D85-02A9EF93FBAB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -335,7 +333,6 @@ Global {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} - {D48A03DF-BCD3-4667-8747-2F251347E2B6} = {B56EBCEC-9C50-46A7-848C-65502DE69C5C} {B51126A8-0D75-4A79-867D-10724EC6AC84} = {94207AA6-4923-4183-A558-E0F8196B8CA3} {5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {C9809D59-6665-471E-AD87-5AC624C65892} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 0e0d4202d..235489b1a 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -5,12 +5,25 @@ True False True + + False + True + False True + + + False + True + False True + + False True + + True @@ -18,6 +31,8 @@ + + <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg new file mode 100644 index 000000000..c719a733a Binary files /dev/null and b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg differ diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512 b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512 new file mode 100644 index 000000000..11c0b5549 --- /dev/null +++ b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512 @@ -0,0 +1 @@ +5W20j9jiNog4dHUEt+cCnePb8z6jFEMnkwO4XilajM7FCnen3KTnN/G8PAUGuQieSlTI9MRe0sRYcafLJl900w== \ No newline at end of file diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec new file mode 100644 index 000000000..d685caf66 --- /dev/null +++ b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec @@ -0,0 +1,23 @@ + + + + Microsoft.Orleans.OrleansCodeGenerator.Build + 2.0.0-beta1-fix + Microsoft Orleans Build-time Code Generator + Microsoft + Microsoft + false + true + https://github.com/dotnet/Orleans#license + https://github.com/dotnet/Orleans + https://raw.githubusercontent.com/dotnet/orleans/gh-pages/assets/logo_128.png + Microsoft Orleans build-time code generator to install in all grain interface & implementation projects. + © Microsoft Corporation. All rights reserved. + Orleans Cloud-Computing Actor-Model Actors Distributed-Systems C# .NET + + + + + + + \ No newline at end of file diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg new file mode 100644 index 000000000..a90f02b9d Binary files /dev/null and b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg differ diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512 b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512 new file mode 100644 index 000000000..6500d397d --- /dev/null +++ b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512 @@ -0,0 +1 @@ +mBHlGWl+bNTPP463JBEB/dftmdZKQRD8X72F7lsTFqYWddW5Ytp1gbzChCxW0d/Pt71KLF6XrVmyecbFlNdFBA== \ No newline at end of file diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec new file mode 100644 index 000000000..cf2043a3f --- /dev/null +++ b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec @@ -0,0 +1,25 @@ + + + + OrleansDashboard + 2.0.0-beta3 + OrleansContrib + OrleansContrib + false + https://opensource.org/licenses/MIT + https://github.com/OrleansContrib/OrleansDashboard + http://dotnet.github.io/orleans/assets/logo.png + An admin dashboard for Microsoft Orleans + Copyright © 2017 + orleans dashboard metrics monitor + + + + + + + + + + + \ No newline at end of file diff --git a/nuget.exe b/nuget.exe new file mode 100644 index 000000000..ec1309c7a Binary files /dev/null and b/nuget.exe differ diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 28e17d013..9f6f0ed73 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System.Diagnostics.Contracts; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps @@ -13,8 +14,8 @@ namespace Squidex.Domain.Apps.Core.Apps public sealed class AppClient { private readonly string secret; - private string name; - private AppClientPermission permission; + private readonly string name; + private readonly AppClientPermission permission; public string Name { @@ -42,18 +43,20 @@ namespace Squidex.Domain.Apps.Core.Apps this.permission = permission; } - public void Update(AppClientPermission newPermission) + [Pure] + public AppClient Update(AppClientPermission newPermission) { Guard.Enum(newPermission, nameof(newPermission)); - permission = newPermission; + return new AppClient(name, secret, newPermission); } - public void Rename(string newName) + [Pure] + public AppClient Rename(string newName) { Guard.NotNullOrEmpty(newName, nameof(newName)); - name = newName; + return new AppClient(newName, secret, permission); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index a92de925b..bd606ea0d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -6,32 +6,75 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; +using System.Diagnostics.Contracts; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { - public sealed class AppClients : DictionaryBase + public sealed class AppClients : DictionaryWrapper { - public void Add(string id, AppClient client) + public static readonly AppClients Empty = new AppClients(); + + private AppClients() + : base(ImmutableDictionary.Empty) + { + } + + public AppClients(ImmutableDictionary inner) + : base(inner) + { + } + + [Pure] + public AppClients Add(string id, AppClient client) { Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNull(client, nameof(client)); - Inner.Add(id, client); + return new AppClients(Inner.Add(id, client)); } - public void Add(string id, string secret) + [Pure] + public AppClients Add(string id, string secret) { Guard.NotNullOrEmpty(id, nameof(id)); - Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor)); + return new AppClients(Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor))); } - public void Revoke(string id) + [Pure] + public AppClients Revoke(string id) { Guard.NotNullOrEmpty(id, nameof(id)); - Inner.Remove(id); + return new AppClients(Inner.Remove(id)); + } + + [Pure] + public AppClients Rename(string id, string newName) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + if (!TryGetValue(id, out var client)) + { + return this; + } + + return new AppClients(Inner.SetItem(id, client.Rename(newName))); + } + + [Pure] + public AppClients Update(string id, AppClientPermission permission) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + if (!TryGetValue(id, out var client)) + { + return this; + } + + return new AppClients(Inner.SetItem(id, client.Update(permission))); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs index 9c4ce924d..ffdd82c3f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs @@ -6,25 +6,41 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; +using System.Diagnostics.Contracts; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { - public sealed class AppContributors : DictionaryBase + public sealed class AppContributors : DictionaryWrapper { - public void Assign(string contributorId, AppContributorPermission permission) + public static readonly AppContributors Empty = new AppContributors(); + + private AppContributors() + : base(ImmutableDictionary.Empty) + { + } + + public AppContributors(ImmutableDictionary inner) + : base(inner) + { + } + + [Pure] + public AppContributors Assign(string contributorId, AppContributorPermission permission) { Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); Guard.Enum(permission, nameof(permission)); - Inner[contributorId] = permission; + return new AppContributors(Inner.SetItem(contributorId, permission)); } - public void Remove(string contributorId) + [Pure] + public AppContributors Remove(string contributorId) { Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - Inner.Remove(contributorId); + return new AppContributors(Inner.Remove(contributorId)); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs index 16570c87b..1aa4d4eca 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -6,7 +6,9 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; +using System.Collections.Immutable; using Newtonsoft.Json; using Squidex.Infrastructure.Json; @@ -26,18 +28,11 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - protected override AppClients ReadValue(JsonReader reader, JsonSerializer serializer) + protected override AppClients ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { var json = serializer.Deserialize>(reader); - var clients = new AppClients(); - - foreach (var client in json) - { - clients.Add(client.Key, client.Value.ToClient()); - } - - return clients; + return new AppClients(json.ToImmutableDictionary(x => x.Key, x => x.Value.ToClient())); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs index efa326377..76baa1710 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs @@ -6,7 +6,9 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; +using System.Collections.Immutable; using Newtonsoft.Json; using Squidex.Infrastructure.Json; @@ -26,18 +28,11 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - protected override AppContributors ReadValue(JsonReader reader, JsonSerializer serializer) + protected override AppContributors ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { var json = serializer.Deserialize>(reader); - var contributors = new AppContributors(); - - foreach (var contributor in json) - { - contributors.Assign(contributor.Key, contributor.Value); - } - - return contributors; + return new AppContributors(json.ToImmutableDictionary()); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs index f23beded4..8ec216f76 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json if (Master != null) { - result.MakeMaster(Master); + result = result.MakeMaster(Master); } return result; diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs index 98dd331cc..d720dfca8 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - protected override LanguagesConfig ReadValue(JsonReader reader, JsonSerializer serializer) + protected override LanguagesConfig ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { var json = serializer.Deserialize(reader); diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index d9c2c24e9..aa3a29666 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -10,83 +10,108 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.Contracts; using System.Linq; using Squidex.Infrastructure; -#pragma warning disable IDE0016 // Use 'throw' expression - namespace Squidex.Domain.Apps.Core.Apps { public sealed class LanguagesConfig : IFieldPartitioning { - private State state; + public static readonly LanguagesConfig Empty = new LanguagesConfig(ImmutableDictionary.Empty, null, false); + public static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN); + + private readonly ImmutableDictionary languages; + private readonly LanguageConfig master; public LanguageConfig Master { - get { return state.Master; } + get { return master; } } IFieldPartitionItem IFieldPartitioning.Master { - get { return state.Master; } + get { return master; } } IEnumerator IEnumerable.GetEnumerator() { - return state.Languages.Values.GetEnumerator(); + return languages.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return state.Languages.Values.GetEnumerator(); + return languages.Values.GetEnumerator(); } public int Count { - get { return state.Languages.Count; } + get { return languages.Count; } } - private LanguagesConfig(ICollection configs) + private LanguagesConfig(ImmutableDictionary languages, LanguageConfig master, bool checkMaster = true) { - Guard.NotNull(configs, nameof(configs)); + if (checkMaster) + { + this.master = master ?? throw new InvalidOperationException("Config has no master language."); + } + + foreach (var languageConfig in languages.Values) + { + foreach (var fallback in languageConfig.LanguageFallbacks) + { + if (!languages.ContainsKey(fallback)) + { + var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; - state = new State(configs.ToImmutableDictionary(x => x.Language), configs.FirstOrDefault()); + throw new InvalidOperationException(message); + } + } + } + + this.languages = languages; } - public static LanguagesConfig Build(params LanguageConfig[] configs) + public static LanguagesConfig Build(ICollection configs) { Guard.NotNull(configs, nameof(configs)); - return new LanguagesConfig(configs); + return new LanguagesConfig(configs.ToImmutableDictionary(x => x.Language), configs.FirstOrDefault()); } - public static LanguagesConfig Build(params Language[] languages) + public static LanguagesConfig Build(params LanguageConfig[] configs) { - Guard.NotNull(languages, nameof(languages)); + return Build(configs?.ToList()); + } - return new LanguagesConfig(languages.Select(x => new LanguageConfig(x, false)).ToList()); + public static LanguagesConfig Build(params Language[] languages) + { + return Build(languages?.Select(x => new LanguageConfig(x)).ToList()); } - public void MakeMaster(Language language) + [Pure] + public LanguagesConfig MakeMaster(Language language) { Guard.NotNull(language, nameof(language)); - state = new State(state.Languages, state.Languages[language]); + return new LanguagesConfig(languages, languages[language]); } - public void Set(LanguageConfig config) + [Pure] + public LanguagesConfig Set(LanguageConfig config) { Guard.NotNull(config, nameof(config)); - state = new State(state.Languages.SetItem(config.Language, config), state.Master?.Language == config.Language ? config : state.Master); + return new LanguagesConfig(languages.SetItem(config.Language, config), Master?.Language == config.Language ? config : Master); } - public void Remove(Language language) + [Pure] + public LanguagesConfig Remove(Language language) { Guard.NotNull(language, nameof(language)); var newLanguages = - state.Languages.Values.Where(x => x.Language != language) + languages.Values.Where(x => x.Language != language) .Select(config => { return new LanguageConfig( @@ -97,26 +122,26 @@ namespace Squidex.Domain.Apps.Core.Apps .ToImmutableDictionary(x => x.Language); var newMaster = - state.Master.Language != language ? - state.Master : + Master.Language != language ? + Master : newLanguages.Values.FirstOrDefault(); - state = new State(newLanguages, newMaster); + return new LanguagesConfig(newLanguages, newMaster); } public bool Contains(Language language) { - return language != null && state.Languages.ContainsKey(language); + return language != null && languages.ContainsKey(language); } public bool TryGetConfig(Language language, out LanguageConfig config) { - return state.Languages.TryGetValue(language, out config); + return languages.TryGetValue(language, out config); } public bool TryGetItem(string key, out IFieldPartitionItem item) { - if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value)) + if (Language.IsValidLanguage(key) && languages.TryGetValue(key, out var value)) { item = value; @@ -130,38 +155,6 @@ namespace Squidex.Domain.Apps.Core.Apps } } - private sealed class State - { - public ImmutableDictionary Languages { get; } - - public LanguageConfig Master { get; } - - public State(ImmutableDictionary languages, LanguageConfig master) - { - foreach (var languageConfig in languages.Values) - { - foreach (var fallback in languageConfig.LanguageFallbacks) - { - if (!languages.ContainsKey(fallback)) - { - var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; - - throw new InvalidOperationException(message); - } - } - } - - Languages = languages; - - if (master == null) - { - throw new InvalidOperationException("Config has no master language."); - } - - this.Master = master; - } - } - public PartitionResolver ToResolver() { return partitioning => diff --git a/src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs b/src/Squidex.Domain.Apps.Core.Model/DictionaryWrapper{TKey,TValue}.cs similarity index 78% rename from src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs rename to src/Squidex.Domain.Apps.Core.Model/DictionaryWrapper{TKey,TValue}.cs index 6b3734c6a..8922cfa48 100644 --- a/src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs +++ b/src/Squidex.Domain.Apps.Core.Model/DictionaryWrapper{TKey,TValue}.cs @@ -8,12 +8,13 @@ using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; namespace Squidex.Domain.Apps.Core { - public abstract class DictionaryBase : IReadOnlyDictionary + public abstract class DictionaryWrapper : IReadOnlyDictionary { - private readonly Dictionary inner = new Dictionary(); + private readonly ImmutableDictionary inner; public TValue this[TKey key] { @@ -35,11 +36,16 @@ namespace Squidex.Domain.Apps.Core get { return inner.Count; } } - protected Dictionary Inner + protected ImmutableDictionary Inner { get { return inner; } } + protected DictionaryWrapper(ImmutableDictionary inner) + { + this.inner = inner; + } + public bool ContainsKey(TKey key) { return inner.ContainsKey(key); diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs index 09718ad8f..974855340 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs @@ -14,9 +14,36 @@ namespace Squidex.Domain.Apps.Core.Rules.Actions [TypeName(nameof(WebhookAction))] public sealed class WebhookAction : RuleAction { - public Uri Url { get; set; } + private Uri url; + private string sharedSecret; - public string SharedSecret { get; set; } + public Uri Url + { + get + { + return url; + } + set + { + ThrowIfFrozen(); + + url = value; + } + } + + public string SharedSecret + { + get + { + return sharedSecret; + } + set + { + ThrowIfFrozen(); + + sharedSecret = value; + } + } public override T Accept(IRuleActionVisitor visitor) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs index 8c999b7ae..3b4d284bd 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Rules.Actions; namespace Squidex.Domain.Apps.Core.Rules { - public interface IRuleActionVisitor + public interface IRuleActionVisitor { T Visit(WebhookAction action); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs index b91c7e604..ccbb1c4c6 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Rules.Triggers; namespace Squidex.Domain.Apps.Core.Rules { - public interface IRuleTriggerVisitor + public interface IRuleTriggerVisitor { T Visit(ContentChangedTrigger trigger); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs index cd3e9707f..176820ef5 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/RuleConverter.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; using Squidex.Infrastructure.Json; @@ -13,14 +14,14 @@ namespace Squidex.Domain.Apps.Core.Rules.Json { public sealed class RuleConverter : JsonClassConverter { - protected override Rule ReadValue(JsonReader reader, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, Rule value, JsonSerializer serializer) { - return serializer.Deserialize(reader).ToRule(); + serializer.Serialize(writer, new JsonRule(value)); } - protected override void WriteValue(JsonWriter writer, Rule value, JsonSerializer serializer) + protected override Rule ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - serializer.Serialize(writer, new JsonRule(value)); + return serializer.Deserialize(reader).ToRule(); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index 8595d84a8..7bec58f87 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -7,11 +7,12 @@ // ========================================================================== using System; +using System.Diagnostics.Contracts; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Rules { - public sealed class Rule + public sealed class Rule : Cloneable { private RuleTrigger trigger; private RuleAction action; @@ -41,17 +42,26 @@ namespace Squidex.Domain.Apps.Core.Rules this.action = action; } - public void Enable() + [Pure] + public Rule Enable() { - this.isEnabled = true; + return Clone(clone => + { + clone.isEnabled = true; + }); } - public void Disable() + [Pure] + public Rule Disable() { - this.isEnabled = false; + return Clone(clone => + { + clone.isEnabled = false; + }); } - public void Update(RuleTrigger newTrigger) + [Pure] + public Rule Update(RuleTrigger newTrigger) { Guard.NotNull(newTrigger, nameof(newTrigger)); @@ -60,10 +70,14 @@ namespace Squidex.Domain.Apps.Core.Rules throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); } - trigger = newTrigger; + return Clone(clone => + { + clone.trigger = newTrigger; + }); } - public void Update(RuleAction newAction) + [Pure] + public Rule Update(RuleAction newAction) { Guard.NotNull(newAction, nameof(newAction)); @@ -72,7 +86,10 @@ namespace Squidex.Domain.Apps.Core.Rules throw new ArgumentException("New action has another type.", nameof(newAction)); } - action = newAction; + return Clone(clone => + { + clone.action = newAction; + }); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs index 483abdff9..d2a5ccfc8 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -6,9 +6,11 @@ // All rights reserved. // ========================================================================== +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Rules { - public abstract class RuleAction + public abstract class RuleAction : Freezable { public abstract T Accept(IRuleActionVisitor visitor); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs index 15284960c..9f996553f 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -6,9 +6,11 @@ // All rights reserved. // ========================================================================== +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Rules { - public abstract class RuleTrigger + public abstract class RuleTrigger : Freezable { public abstract T Accept(IRuleTriggerVisitor visitor); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs index cc11d969c..7f8691725 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System.Collections.Immutable; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Rules.Triggers @@ -14,7 +14,21 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers [TypeName(nameof(ContentChangedTrigger))] public sealed class ContentChangedTrigger : RuleTrigger { - public List Schemas { get; set; } + private ImmutableList schemas; + + public ImmutableList Schemas + { + get + { + return schemas; + } + set + { + ThrowIfFrozen(); + + schemas = value; + } + } public override T Accept(IRuleTriggerVisitor visitor) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs index cf1ad40a6..c4de1e7f1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -13,13 +13,45 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(AssetsField))] public sealed class AssetsFieldProperties : FieldProperties { - public int? MinItems { get; set; } + private int? minItems; + private int? maxItems; - public int? MaxItems { get; set; } + public int? MinItems + { + get + { + return minItems; + } + set + { + ThrowIfFrozen(); + + minItems = value; + } + } + + public int? MaxItems + { + get + { + return maxItems; + } + set + { + ThrowIfFrozen(); + + maxItems = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new AssetsField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs index f773ce620..c6817f309 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -13,13 +13,45 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(BooleanField))] public sealed class BooleanFieldProperties : FieldProperties { - public bool? DefaultValue { get; set; } + private BooleanFieldEditor editor; + private bool? defaultValue; - public BooleanFieldEditor Editor { get; set; } + public bool? DefaultValue + { + get + { + return defaultValue; + } + set + { + ThrowIfFrozen(); + + defaultValue = value; + } + } + + public BooleanFieldEditor Editor + { + get + { + return editor; + } + set + { + ThrowIfFrozen(); + + editor = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new BooleanField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs index 9c03a46c8..382fc8430 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -14,19 +14,90 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(DateTimeField))] public sealed class DateTimeFieldProperties : FieldProperties { - public Instant? MaxValue { get; set; } + private DateTimeFieldEditor editor; + private DateTimeCalculatedDefaultValue? calculatedDefaultValue; + private Instant? maxValue; + private Instant? minValue; + private Instant? defaultValue; - public Instant? MinValue { get; set; } + public Instant? MaxValue + { + get + { + return maxValue; + } + set + { + ThrowIfFrozen(); + + maxValue = value; + } + } + + public Instant? MinValue + { + get + { + return minValue; + } + set + { + ThrowIfFrozen(); - public Instant? DefaultValue { get; set; } + minValue = value; + } + } - public DateTimeFieldEditor Editor { get; set; } + public Instant? DefaultValue + { + get + { + return defaultValue; + } + set + { + ThrowIfFrozen(); - public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + defaultValue = value; + } + } + + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue + { + get + { + return calculatedDefaultValue; + } + set + { + ThrowIfFrozen(); + + calculatedDefaultValue = value; + } + } + + public DateTimeFieldEditor Editor + { + get + { + return editor; + } + set + { + ThrowIfFrozen(); + + editor = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new DateTimeField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs index 2f9edf612..d10500fc1 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs @@ -6,11 +6,12 @@ // All rights reserved. // ========================================================================== +using System.Diagnostics.Contracts; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Schemas { - public abstract class Field + public abstract class Field : Cloneable { private readonly long fieldId; private readonly Partitioning partitioning; @@ -63,32 +64,52 @@ namespace Squidex.Domain.Apps.Core.Schemas this.partitioning = partitioning; } - public void Lock() + [Pure] + public Field Lock() { - isLocked = true; + return Clone(clone => + { + clone.isLocked = true; + }); } - public void Hide() + [Pure] + public Field Hide() { - isHidden = true; + return Clone(clone => + { + clone.isHidden = true; + }); } - public void Show() + [Pure] + public Field Show() { - isHidden = false; + return Clone(clone => + { + clone.isHidden = false; + }); } - public void Disable() + [Pure] + public Field Disable() { - isDisabled = true; + return Clone(clone => + { + clone.isDisabled = true; + }); } - public void Enable() + [Pure] + public Field Enable() { - isDisabled = false; + return Clone(clone => + { + clone.isDisabled = false; + }); } - public abstract void Update(FieldProperties newProperties); + public abstract Field Update(FieldProperties newProperties); public abstract T Accept(IFieldVisitor visitor); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index c975e4ef9..aa530df70 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -10,12 +10,54 @@ namespace Squidex.Domain.Apps.Core.Schemas { public abstract class FieldProperties : NamedElementPropertiesBase { - public bool IsRequired { get; set; } + private bool isRequired; + private bool isListField; + private string placeholder; - public bool IsListField { get; set; } + public bool IsRequired + { + get + { + return isRequired; + } + set + { + ThrowIfFrozen(); - public string Placeholder { get; set; } + isRequired = value; + } + } + + public bool IsListField + { + get + { + return isListField; + } + set + { + ThrowIfFrozen(); + + isListField = value; + } + } + + public string Placeholder + { + get + { + return placeholder; + } + set + { + ThrowIfFrozen(); + + placeholder = value; + } + } public abstract T Accept(IFieldPropertiesVisitor visitor); + + public abstract Field CreateField(long id, string name, Partitioning partitioning); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs index 710581ef1..5c6cecb70 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs @@ -17,29 +17,7 @@ namespace Squidex.Domain.Apps.Core.Schemas private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties); private readonly TypeNameRegistry typeNameRegistry; - private readonly Dictionary fieldsByPropertyType = new Dictionary(); - - private sealed class Registered - { - private readonly FactoryFunction fieldFactory; - private readonly Type propertiesType; - - public Type PropertiesType - { - get { return propertiesType; } - } - - public Registered(FactoryFunction fieldFactory, Type propertiesType) - { - this.fieldFactory = fieldFactory; - this.propertiesType = propertiesType; - } - - public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) - { - return fieldFactory(id, name, partitioning, properties); - } - } + private readonly Dictionary fieldsByPropertyType = new Dictionary(); public FieldRegistry(TypeNameRegistry typeNameRegistry) { @@ -47,69 +25,39 @@ namespace Squidex.Domain.Apps.Core.Schemas this.typeNameRegistry = typeNameRegistry; - Add( - (id, name, partitioning, properties) => - new BooleanField(id, name, partitioning, (BooleanFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new NumberField(id, name, partitioning, (NumberFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new StringField(id, name, partitioning, (StringFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new JsonField(id, name, partitioning, (JsonFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new AssetsField(id, name, partitioning, (AssetsFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new GeolocationField(id, name, partitioning, (GeolocationFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new ReferencesField(id, name, partitioning, (ReferencesFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new DateTimeField(id, name, partitioning, (DateTimeFieldProperties)properties)); - - Add( - (id, name, partitioning, properties) => - new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); + RegisterField(); typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); } - private void Add(FactoryFunction fieldFactory) + private void RegisterField() { - Guard.NotNull(fieldFactory, nameof(fieldFactory)); - - typeNameRegistry.Map(typeof(TFieldProperties)); - - var registered = new Registered(fieldFactory, typeof(TFieldProperties)); + typeNameRegistry.Map(typeof(T)); - fieldsByPropertyType[registered.PropertiesType] = registered; + fieldsByPropertyType[typeof(T)] = (id, name, partitioning, properties) => properties.CreateField(id, name, partitioning); } public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) { Guard.NotNull(properties, nameof(properties)); - var registered = fieldsByPropertyType.GetOrDefault(properties.GetType()); + var factory = fieldsByPropertyType.GetOrDefault(properties.GetType()); - if (registered == null) + if (factory == null) { throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported."); } - return registered.CreateField(id, name, partitioning, properties); + return factory(id, name, partitioning, properties); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs index edc05dbb2..3a9e2d18a 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs @@ -1,5 +1,5 @@ // ========================================================================== -// Field_Generic.cs +// Field{T}.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,7 +7,9 @@ // ========================================================================== using System; +using System.Diagnostics.Contracts; using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Schemas { public abstract class Field : Field where T : FieldProperties, new() @@ -30,13 +32,19 @@ namespace Squidex.Domain.Apps.Core.Schemas Guard.NotNull(properties, nameof(properties)); this.properties = properties; + this.properties.Freeze(); } - public override void Update(FieldProperties newProperties) + [Pure] + public override Field Update(FieldProperties newProperties) { var typedProperties = ValidateProperties(newProperties); - properties = typedProperties; + return Clone>(clone => + { + clone.properties = typedProperties; + clone.properties.Freeze(); + }); } private T ValidateProperties(FieldProperties newProperties) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs index 5e4b34d4e..e01fddcb4 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -13,11 +13,30 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(GeolocationField))] public sealed class GeolocationFieldProperties : FieldProperties { - public GeolocationFieldEditor Editor { get; set; } + private GeolocationFieldEditor editor; + + public GeolocationFieldEditor Editor + { + get + { + return editor; + } + set + { + ThrowIfFrozen(); + + editor = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new GeolocationField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs index f2c6695bd..c1c723cf0 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json { public sealed class JsonSchemaModel { + private static readonly Field[] Empty = new Field[0]; + [JsonProperty] public string Name { get; set; } @@ -54,46 +56,40 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json public Schema ToSchema(FieldRegistry fieldRegistry) { - var schema = new Schema(Name); + Field[] fields = Empty; if (Fields != null) { - foreach (var fieldModel in Fields) + fields = new Field[Fields.Count]; + + for (var i = 0; i < fields.Length; i++) { + var fieldModel = Fields[i]; + var parititonKey = new Partitioning(fieldModel.Partitioning); var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties); if (fieldModel.IsDisabled) { - field.Disable(); + field = field.Disable(); } if (fieldModel.IsLocked) { - field.Lock(); + field = field.Lock(); } if (fieldModel.IsHidden) { - field.Hide(); + field = field.Hide(); } - schema.AddField(field); + fields[i] = field; } } - if (IsPublished) - { - schema.Publish(); - } - - if (Properties != null) - { - schema.Update(Properties); - } - - return schema; + return new Schema(Name, fields, Properties, IsPublished); } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs index 317afa717..6db657dc2 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; @@ -28,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json serializer.Serialize(writer, new JsonSchemaModel(value)); } - protected override Schema ReadValue(JsonReader reader, JsonSerializer serializer) + protected override Schema ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { return serializer.Deserialize(reader).ToSchema(fieldRegistry); } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs index c432be09a..5c4ffd918 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -17,5 +17,10 @@ namespace Squidex.Domain.Apps.Core.Schemas { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new JsonField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs index 7f5a9d572..ca23aec3d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -6,12 +6,41 @@ // All rights reserved. // ========================================================================== +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Schemas { - public abstract class NamedElementPropertiesBase + public abstract class NamedElementPropertiesBase : Freezable { - public string Label { get; set; } + private string label; + private string hints; + + public string Label + { + get + { + return label; + } + set + { + ThrowIfFrozen(); + + label = value; + } + } + + public string Hints + { + get + { + return hints; + } + set + { + ThrowIfFrozen(); - public string Hints { get; set; } + hints = value; + } + } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index ce1703faa..f7d6af843 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Schemas @@ -13,19 +14,90 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(NumberField))] public sealed class NumberFieldProperties : FieldProperties { - public double? MaxValue { get; set; } + private double? maxValue; + private double? minValue; + private double? defaultValue; + private ImmutableList allowedValues; + private NumberFieldEditor editor; - public double? MinValue { get; set; } + public double? MaxValue + { + get + { + return maxValue; + } + set + { + ThrowIfFrozen(); + + maxValue = value; + } + } + + public double? MinValue + { + get + { + return minValue; + } + set + { + ThrowIfFrozen(); - public double? DefaultValue { get; set; } + minValue = value; + } + } - public double[] AllowedValues { get; set; } + public double? DefaultValue + { + get + { + return defaultValue; + } + set + { + ThrowIfFrozen(); - public NumberFieldEditor Editor { get; set; } + defaultValue = value; + } + } + + public ImmutableList AllowedValues + { + get + { + return allowedValues; + } + set + { + ThrowIfFrozen(); + + allowedValues = value; + } + } + + public NumberFieldEditor Editor + { + get + { + return editor; + } + set + { + ThrowIfFrozen(); + + editor = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new NumberField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 8a4f76192..3faa12a26 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -14,15 +14,60 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(ReferencesField))] public sealed class ReferencesFieldProperties : FieldProperties { - public int? MinItems { get; set; } + private int? minItems; + private int? maxItems; + private Guid schemaId; - public int? MaxItems { get; set; } + public int? MinItems + { + get + { + return minItems; + } + set + { + ThrowIfFrozen(); + + minItems = value; + } + } - public Guid SchemaId { get; set; } + public int? MaxItems + { + get + { + return maxItems; + } + set + { + ThrowIfFrozen(); + + maxItems = value; + } + } + + public Guid SchemaId + { + get + { + return schemaId; + } + set + { + ThrowIfFrozen(); + + schemaId = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new ReferencesField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index 137e49783..44404f001 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -8,18 +8,20 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; using System.Linq; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Schemas { - public sealed class Schema + public sealed class Schema : Cloneable { private readonly string name; - private readonly List fieldsOrdered = new List(); - private readonly Dictionary fieldsById = new Dictionary(); - private readonly Dictionary fieldsByName = new Dictionary(); - private SchemaProperties properties = new SchemaProperties(); + private ImmutableArray fieldsOrdered = ImmutableArray.Empty; + private ImmutableDictionary fieldsById; + private ImmutableDictionary fieldsByName; + private SchemaProperties properties; private bool isPublished; public string Name @@ -39,12 +41,42 @@ namespace Squidex.Domain.Apps.Core.Schemas public IReadOnlyDictionary FieldsById { - get { return fieldsById; } + get + { + if (fieldsById == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsById = ImmutableDictionary.Empty; + } + else + { + fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id); + } + } + + return fieldsById; + } } public IReadOnlyDictionary FieldsByName { - get { return fieldsByName; } + get + { + if (fieldsByName == null) + { + if (fieldsOrdered.Length == 0) + { + fieldsByName = ImmutableDictionary.Empty; + } + else + { + fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name); + } + } + + return fieldsByName; + } } public SchemaProperties Properties @@ -52,69 +84,177 @@ namespace Squidex.Domain.Apps.Core.Schemas get { return properties; } } - public void Publish() + public Schema(string name, SchemaProperties properties = null) { - isPublished = true; - } + Guard.NotNullOrEmpty(name, nameof(name)); - public void Unpublish() - { - isPublished = false; + this.name = name; + + this.properties = properties ?? new SchemaProperties(); + this.properties.Freeze(); } - public Schema(string name) + public Schema(string name, Field[] fields, SchemaProperties properties, bool isPublished) + : this(name, properties) { Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNull(fields, nameof(fields)); - this.name = name; + this.isPublished = isPublished; + + fieldsOrdered = ImmutableArray.Create(fields); + } + + protected override void OnCloned() + { + fieldsById = null; + fieldsByName = null; } - public void Update(SchemaProperties newProperties) + [Pure] + public Schema Update(SchemaProperties newProperties) { Guard.NotNull(newProperties, nameof(newProperties)); - properties = newProperties; + return Clone(clone => + { + clone.properties = newProperties; + clone.properties.Freeze(); + }); + } + + [Pure] + public Schema UpdateField(long fieldId, FieldProperties newProperties) + { + return UpdateField(fieldId, field => + { + return field.Update(newProperties); + }); } - public void DeleteField(long fieldId) + [Pure] + public Schema LockField(long fieldId) { - if (!fieldsById.TryGetValue(fieldId, out var field)) + return UpdateField(fieldId, field => { - return; + return field.Lock(); + }); + } + + [Pure] + public Schema DisableField(long fieldId) + { + return UpdateField(fieldId, field => + { + return field.Disable(); + }); + } + + [Pure] + public Schema EnableField(long fieldId) + { + return UpdateField(fieldId, field => + { + return field.Enable(); + }); + } + + [Pure] + public Schema HideField(long fieldId) + { + return UpdateField(fieldId, field => + { + return field.Hide(); + }); + } + + [Pure] + public Schema ShowField(long fieldId) + { + return UpdateField(fieldId, field => + { + return field.Show(); + }); + } + + [Pure] + public Schema Publish() + { + return Clone(clone => + { + clone.isPublished = true; + }); + } + + [Pure] + public Schema Unpublish() + { + return Clone(clone => + { + clone.isPublished = false; + }); + } + + [Pure] + public Schema DeleteField(long fieldId) + { + if (!FieldsById.TryGetValue(fieldId, out var field)) + { + return this; } - fieldsById.Remove(fieldId); - fieldsByName.Remove(field.Name); - fieldsOrdered.Remove(field); + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.Remove(field); + }); } - public void ReorderFields(List ids) + [Pure] + public Schema ReorderFields(List ids) { Guard.NotNull(ids, nameof(ids)); - if (ids.Count != fieldsOrdered.Count || ids.Any(x => !fieldsById.ContainsKey(x))) + if (ids.Count != fieldsOrdered.Length || ids.Any(x => !FieldsById.ContainsKey(x))) { throw new ArgumentException("Ids must cover all fields.", nameof(ids)); } - var fields = fieldsOrdered.ToList(); - - fieldsOrdered.Clear(); - fieldsOrdered.AddRange(fields.OrderBy(f => ids.IndexOf(f.Id))); + return Clone(clone => + { + clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableArray(); + }); } - public void AddField(Field field) + [Pure] + public Schema AddField(Field field) { Guard.NotNull(field, nameof(field)); - if (fieldsByName.ContainsKey(field.Name) || fieldsById.ContainsKey(field.Id)) + if (FieldsByName.ContainsKey(field.Name) || FieldsById.ContainsKey(field.Id)) { throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field)); } - fieldsById.Add(field.Id, field); - fieldsByName.Add(field.Name, field); - fieldsOrdered.Add(field); + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Add(field); + }); + } + + [Pure] + public Schema UpdateField(long fieldId, Func updater) + { + Guard.NotNull(updater, nameof(updater)); + + if (!FieldsById.TryGetValue(fieldId, out var field)) + { + return this; + } + + return Clone(clone => + { + clone.fieldsOrdered = clone.fieldsOrdered.Replace(field, updater(field)); + }); } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index 5263cc2e7..80c2d6cbc 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Schemas @@ -13,23 +14,120 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(StringField))] public sealed class StringFieldProperties : FieldProperties { - public int? MinLength { get; set; } + private int? minLength; + private int? maxLength; + private string pattern; + private string patternMessage; + private string defaultValue; + private ImmutableList allowedValues; + private StringFieldEditor editor; - public int? MaxLength { get; set; } + public int? MinLength + { + get + { + return minLength; + } + set + { + ThrowIfFrozen(); - public string DefaultValue { get; set; } + minLength = value; + } + } - public string Pattern { get; set; } + public int? MaxLength + { + get + { + return maxLength; + } + set + { + ThrowIfFrozen(); - public string PatternMessage { get; set; } + maxLength = value; + } + } - public string[] AllowedValues { get; set; } + public string DefaultValue + { + get + { + return defaultValue; + } + set + { + ThrowIfFrozen(); - public StringFieldEditor Editor { get; set; } + defaultValue = value; + } + } + + public string Pattern + { + get + { + return pattern; + } + set + { + ThrowIfFrozen(); + + pattern = value; + } + } + + public string PatternMessage + { + get + { + return patternMessage; + } + set + { + ThrowIfFrozen(); + + patternMessage = value; + } + } + + public ImmutableList AllowedValues + { + get + { + return allowedValues; + } + set + { + ThrowIfFrozen(); + + allowedValues = value; + } + } + + public StringFieldEditor Editor + { + get + { + return editor; + } + set + { + ThrowIfFrozen(); + + editor = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new StringField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs index 9e10e634b..579c65ca5 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -13,13 +13,45 @@ namespace Squidex.Domain.Apps.Core.Schemas [TypeName(nameof(TagsField))] public sealed class TagsFieldProperties : FieldProperties { - public int? MinItems { get; set; } + private int? minItems; + private int? maxItems; - public int? MaxItems { get; set; } + public int? MinItems + { + get + { + return minItems; + } + set + { + ThrowIfFrozen(); + + minItems = value; + } + } + + public int? MaxItems + { + get + { + return maxItems; + } + set + { + ThrowIfFrozen(); + + maxItems = value; + } + } public override T Accept(IFieldPropertiesVisitor visitor) { return visitor.Visit(this); } + + public override Field CreateField(long id, string name, Partitioning partitioning) + { + return new TagsField(id, name, partitioning, this); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index f70c8b9f0..86c6c1781 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds public static IEnumerable Visit(AssetsField field, JToken value) { - IEnumerable result = null; + IEnumerable result; try { result = value?.ToObject>(); @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds private static IEnumerable Visit(ReferencesField field, JToken value) { - IEnumerable result = null; + IEnumerable result; try { result = value?.ToObject>() ?? Enumerable.Empty(); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs index aae33d8ca..afc865466 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -20,7 +20,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Http; -namespace Squidex.Domain.Apps.Core.HandleRules.ActionHandlers +namespace Squidex.Domain.Apps.Core.HandleRules.Actions { public sealed class WebhookActionHandler : RuleActionHandler { diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 6ab62dc73..70f1db7d9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -116,11 +116,11 @@ namespace Squidex.Domain.Apps.Core.Scripting } catch (ParserException ex) { - throw new ValidationException($"Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message)); + throw new ValidationException("Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message)); } catch (JavaScriptException ex) { - throw new ValidationException($"Failed to execute script with javascript error.", new ValidationError(ex.Message)); + throw new ValidationException("Failed to execute script with javascript error.", new ValidationError(ex.Message)); } } @@ -171,7 +171,7 @@ namespace Squidex.Domain.Apps.Core.Scripting { var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; - throw new ValidationException($"Script rejected the operation.", errors); + throw new ValidationException("Script rejected the operation.", errors); })); } } 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 e5d74893a..e57722909 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 @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs index e6b4a9998..5da60f86e 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs @@ -13,53 +13,47 @@ namespace Squidex.Domain.Apps.Events.Apps.Utils { public static class AppEventDispatcher { - public static void Apply(this AppContributors contributors, AppContributorRemoved @event) + public static AppContributors Apply(this AppContributors contributors, AppContributorRemoved @event) { - contributors.Remove(@event.ContributorId); + return contributors.Remove(@event.ContributorId); } - public static void Apply(this AppContributors contributors, AppContributorAssigned @event) + public static AppContributors Apply(this AppContributors contributors, AppContributorAssigned @event) { - contributors.Assign(@event.ContributorId, @event.Permission); + return contributors.Assign(@event.ContributorId, @event.Permission); } - public static void Apply(this LanguagesConfig languagesConfig, AppLanguageAdded @event) + public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageAdded @event) { - languagesConfig.Set(new LanguageConfig(@event.Language)); + return languagesConfig.Set(new LanguageConfig(@event.Language)); } - public static void Apply(this LanguagesConfig languagesConfig, AppLanguageRemoved @event) + public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageRemoved @event) { - languagesConfig.Remove(@event.Language); + return languagesConfig.Remove(@event.Language); } - public static void Apply(this AppClients clients, AppClientAttached @event) + public static AppClients Apply(this AppClients clients, AppClientAttached @event) { - clients.Add(@event.Id, @event.Secret); + return clients.Add(@event.Id, @event.Secret); } - public static void Apply(this AppClients clients, AppClientRevoked @event) + public static AppClients Apply(this AppClients clients, AppClientRevoked @event) { - clients.Revoke(@event.Id); + return clients.Revoke(@event.Id); } - public static void Apply(this AppClients clients, AppClientRenamed @event) + public static AppClients Apply(this AppClients clients, AppClientRenamed @event) { - if (clients.TryGetValue(@event.Id, out var client)) - { - client.Rename(@event.Name); - } + return clients.Rename(@event.Id, @event.Name); } - public static void Apply(this AppClients clients, AppClientUpdated @event) + public static AppClients Apply(this AppClients clients, AppClientUpdated @event) { - if (clients.TryGetValue(@event.Id, out var client)) - { - client.Update(@event.Permission); - } + return clients.Update(@event.Id, @event.Permission); } - public static void Apply(this LanguagesConfig languagesConfig, AppLanguageUpdated @event) + public static LanguagesConfig Apply(this LanguagesConfig languagesConfig, AppLanguageUpdated @event) { var fallback = @event.Fallback; @@ -70,12 +64,14 @@ namespace Squidex.Domain.Apps.Events.Apps.Utils fallback = fallback.Intersect(existingLangauges).ToList(); } - languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, fallback)); + languagesConfig = languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, fallback)); if (@event.IsMaster) { - languagesConfig.MakeMaster(@event.Language); + languagesConfig = languagesConfig.MakeMaster(@event.Language); } + + return languagesConfig; } } } diff --git a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs index 8de79c181..e85f4eecf 100644 --- a/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Rules/Utils/RuleEventDispatcher.cs @@ -17,27 +17,29 @@ namespace Squidex.Domain.Apps.Events.Rules.Utils return new Rule(@event.Trigger, @event.Action); } - public static void Apply(this Rule rule, RuleUpdated @event) + public static Rule Apply(this Rule rule, RuleUpdated @event) { if (@event.Trigger != null) { - rule.Update(@event.Trigger); + return rule.Update(@event.Trigger); } if (@event.Action != null) { - rule.Update(@event.Action); + return rule.Update(@event.Action); } + + return rule; } - public static void Apply(this Rule rule, RuleEnabled @event) + public static Rule Apply(this Rule rule, RuleEnabled @event) { - rule.Enable(); + return rule.Enable(); } - public static void Apply(this Rule rule, RuleDisabled @event) + public static Rule Apply(this Rule rule, RuleDisabled @event) { - rule.Disable(); + return rule.Disable(); } } } diff --git a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs index 0ff340140..7deea8e63 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils if (@event.Properties != null) { - schema.Update(@event.Properties); + schema = schema.Update(@event.Properties); } if (@event.Fields != null) @@ -38,20 +38,20 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils if (eventField.IsHidden) { - field.Hide(); + field = field.Hide(); } if (eventField.IsDisabled) { - field.Disable(); + field = field.Disable(); } if (eventField.IsLocked) { - field.Lock(); + field = field.Lock(); } - schema.AddField(field); + schema = schema.AddField(field); fieldId++; } @@ -60,91 +60,74 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils return schema; } - public static void Apply(this Schema schema, FieldAdded @event, FieldRegistry registry) + public static Schema Apply(this Schema schema, FieldAdded @event, FieldRegistry registry) { var partitioning = string.Equals(@event.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? Partitioning.Language : Partitioning.Invariant; - var fieldId = @event.FieldId.Id; - var field = registry.CreateField(fieldId, @event.Name, partitioning, @event.Properties); + var field = registry.CreateField(@event.FieldId.Id, @event.Name, partitioning, @event.Properties); - schema.DeleteField(fieldId); - schema.AddField(field); + schema = schema.DeleteField(@event.FieldId.Id); + schema = schema.AddField(field); + + return schema; } - public static void Apply(this Schema schema, FieldUpdated @event) + public static Schema Apply(this Schema schema, FieldUpdated @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Update(@event.Properties); - } + return schema.UpdateField(@event.FieldId.Id, @event.Properties); } - public static void Apply(this Schema schema, FieldLocked @event) + public static Schema Apply(this Schema schema, FieldLocked @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Lock(); - } + return schema.LockField(@event.FieldId.Id); } - public static void Apply(this Schema schema, FieldHidden @event) + public static Schema Apply(this Schema schema, FieldHidden @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Hide(); - } + return schema.HideField(@event.FieldId.Id); } - public static void Apply(this Schema schema, FieldShown @event) + public static Schema Apply(this Schema schema, FieldShown @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Show(); - } + return schema.ShowField(@event.FieldId.Id); } - public static void Apply(this Schema schema, FieldDisabled @event) + public static Schema Apply(this Schema schema, FieldDisabled @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Disable(); - } + return schema.DisableField(@event.FieldId.Id); } - public static void Apply(this Schema schema, FieldEnabled @event) + public static Schema Apply(this Schema schema, FieldEnabled @event) { - if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) - { - field.Enable(); - } + return schema.EnableField(@event.FieldId.Id); } - public static void Apply(this Schema schema, SchemaUpdated @event) + public static Schema Apply(this Schema schema, SchemaUpdated @event) { - schema.Update(@event.Properties); + return schema.Update(@event.Properties); } - public static void Apply(this Schema schema, SchemaFieldsReordered @event) + public static Schema Apply(this Schema schema, SchemaFieldsReordered @event) { - schema.ReorderFields(@event.FieldIds); + return schema.ReorderFields(@event.FieldIds); } - public static void Apply(this Schema schema, FieldDeleted @event) + public static Schema Apply(this Schema schema, FieldDeleted @event) { - schema.DeleteField(@event.FieldId.Id); + return schema.DeleteField(@event.FieldId.Id); } - public static void Apply(this Schema schema, SchemaPublished @event) + public static Schema Apply(this Schema schema, SchemaPublished @event) { - schema.Publish(); + return schema.Publish(); } - public static void Apply(this Schema schema, SchemaUnpublished @event) + public static Schema Apply(this Schema schema, SchemaUnpublished @event) { - schema.Unpublish(); + return schema.Unpublish(); } } } diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index 08e97944f..9e69912e3 100644 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs deleted file mode 100644 index 46e1f5e9e..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// MongoAppEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public sealed class MongoAppEntity : MongoEntity, IAppEntity - { - [BsonRequired] - [BsonElement] - public string Name { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public string PlanId { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public string PlanOwner { get; set; } - - [BsonIgnoreIfDefault] - [BsonElement] - public string[] ContributorIds { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public AppClients Clients { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public AppContributors Contributors { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public LanguagesConfig LanguagesConfig { get; set; } - - public PartitionResolver PartitionResolver - { - get { return LanguagesConfig.ToResolver(); } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs deleted file mode 100644 index f55b7e494..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// MongoAppRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public partial class MongoAppRepository : MongoRepositoryBase, IAppRepository, IAppEventConsumer - { - public MongoAppRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_Apps"; - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection) - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ContributorIds)); - } - - protected override MongoCollectionSettings CollectionSettings() - { - return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; - } - - public async Task> QueryAllAsync(string subjectId) - { - var appEntities = - await Collection.Find(s => s.ContributorIds.Contains(subjectId)) - .ToListAsync(); - - return appEntities; - } - - public async Task FindAppAsync(Guid id) - { - var appEntity = - await Collection.Find(s => s.Id == id) - .FirstOrDefaultAsync(); - - return appEntity; - } - - public async Task FindAppAsync(string name) - { - var appEntity = - await Collection.Find(s => s.Name == name) - .FirstOrDefaultAsync(); - - return appEntity; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs deleted file mode 100644 index 51c627fec..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ /dev/null @@ -1,143 +0,0 @@ -// ========================================================================== -// MongoAppRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public partial class MongoAppRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^app-"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(AppCreated @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - - a.Clients = new AppClients(); - a.Contributors = new AppContributors(); - - a.LanguagesConfig = LanguagesConfig.Build(Language.EN); - }); - } - - protected Task On(AppPlanChanged @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AppClientAttached @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Clients.Apply(@event); - }); - } - - protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Clients.Apply(@event); - }); - } - - protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Clients.Apply(@event); - }); - } - - protected Task On(AppClientUpdated @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Clients.Apply(@event); - }); - } - - protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Contributors.Apply(@event); - }); - } - - protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.Contributors.Apply(@event); - }); - } - - protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.LanguagesConfig.Apply(@event); - }); - } - - protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.LanguagesConfig.Apply(@event); - }); - } - - protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.LanguagesConfig.Apply(@event); - }); - } - - private async Task UpdateAppAsync(AppEvent @event, EnvelopeHeaders headers, Action updater) - { - await Collection.UpdateAsync(@event, headers, a => - { - updater(a); - - a.ContributorIds = a.Contributors.Keys.ToArray(); - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs index 11c54cc36..10d6f31ff 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs @@ -14,7 +14,12 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Read.MongoDb.Assets { - public sealed class MongoAssetEntity : MongoEntity, IAssetEntity + public sealed class MongoAssetEntity : + MongoEntity, + IAssetEntity, + IUpdateableEntityWithVersion, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy { [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs index 141e33e6f..070c535ec 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs @@ -19,7 +19,11 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Read.MongoDb.Contents { - public sealed class MongoContentEntity : IContentEntity, IMongoEntity + public sealed class MongoContentEntity : + IContentEntity, + IUpdateableEntityWithVersion, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy { private NamedContentData data; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs index 5ca896110..b0d763553 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs @@ -19,7 +19,6 @@ using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; @@ -29,7 +28,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents { private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; - private readonly ISchemaProvider schemas; + private readonly IAppProvider appProvider; protected static FilterDefinitionBuilder Filter { @@ -63,13 +62,13 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas) + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) { Guard.NotNull(database, nameof(database)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(appProvider, nameof(appProvider)); this.database = database; - this.schemas = schemas; + this.appProvider = appProvider; } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) @@ -177,11 +176,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents return contentEntity; } - private async Task ForSchemaAsync(Guid appId, Guid schemaId, Func, ISchemaEntity, Task> action) + private async Task ForSchemaAsync(NamedId appId, Guid schemaId, Func, ISchemaEntity, Task> action) { - var collection = GetCollection(appId); + var collection = GetCollection(appId.Id); - var schema = await schemas.FindSchemaByIdAsync(schemaId, true); + var schema = await appProvider.GetSchemaAsync(appId.Name, schemaId, true); if (schema == null) { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 077fa6cd1..fabf77afc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents protected Task On(ContentCreated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.AppId.Id, @event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => { return collection.CreateAsync(@event, headers, content => { @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents protected Task On(ContentUpdated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.AppId.Id, @event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => { var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs index b0563ae70..e09f4206e 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs @@ -14,7 +14,10 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Read.MongoDb.History { - public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, IEntityWithCreatedBy + public sealed class MongoHistoryEventEntity : MongoEntity, + IEntityWithAppRef, + IUpdateableEntityWithVersion, + IUpdateableEntityWithCreatedBy { [BsonRequired] [BsonElement] @@ -40,7 +43,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History [BsonElement] public Dictionary Parameters { get; set; } - RefToken IEntityWithCreatedBy.CreatedBy + RefToken IUpdateableEntityWithCreatedBy.CreatedBy { get { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs index 53368fd51..6b0464bc3 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -83,10 +83,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History if (message != null) { - await Collection.CreateAsync((SquidexEvent)@event.Payload, @event.Headers, entity => + var appEvent = (AppEvent)@event.Payload; + + await Collection.CreateAsync(appEvent, @event.Headers, entity => { entity.Id = Guid.NewGuid(); + entity.AppId = appEvent.AppId.Id; + entity.Version = @event.Headers.EventStreamNumber(); entity.Channel = message.Channel; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs index 461af2374..e3520bd43 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs @@ -12,6 +12,8 @@ using NodaTime; using Squidex.Domain.Apps.Read.History; using Squidex.Infrastructure; +#pragma warning disable RECS0029 // Warns about property or indexer setters and event adders or removers that do not use the value parameter + namespace Squidex.Domain.Apps.Read.MongoDb.History { internal sealed class ParsedHistoryEvent : IHistoryEventEntity @@ -22,26 +24,29 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History public Guid Id { get { return inner.Id; } + set { } } - public Guid EventId + public Instant Created { - get { return inner.Id; } + get { return inner.Created; } + set { } } - public RefToken Actor + public Instant LastModified { - get { return inner.Actor; } + get { return inner.LastModified; } + set { } } - public Instant Created + public RefToken Actor { - get { return inner.Created; } + get { return inner.Actor; } } - public Instant LastModified + public Guid EventId { - get { return inner.LastModified; } + get { return inner.Id; } } public long Version diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs index c9cc5cd8c..4a15254dc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs @@ -18,25 +18,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb { public static class MongoCollectionExtensions { - public static Task CreateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IMongoEntity, new() + public static Task CreateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IEntity, new() { - var entity = EntityMapper.Create(@event, headers); - - updater(entity); + var entity = EntityMapper.Create(@event, headers, updater); return collection.InsertOneIfNotExistsAsync(entity); } - public static async Task CreateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Func updater) where T : class, IMongoEntity, new() - { - var entity = EntityMapper.Create(@event, headers); - - await updater(entity); - - await collection.InsertOneIfNotExistsAsync(entity); - } - - public static async Task UpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IMongoEntity, new() + public static async Task UpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IEntity, new() { var entity = await collection.Find(t => t.Id == headers.AggregateId()) @@ -50,7 +39,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb await collection.UpdateAsync(@event, headers, entity, updater); } - public static async Task TryUpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IMongoEntity, new() + public static async Task TryUpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, Action updater) where T : class, IEntity, new() { var entity = await collection.Find(t => t.Id == headers.AggregateId()) @@ -76,11 +65,9 @@ namespace Squidex.Domain.Apps.Read.MongoDb return false; } - private static async Task UpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, T entity, Action updater) where T : class, IMongoEntity, new() + private static async Task UpdateAsync(this IMongoCollection collection, SquidexEvent @event, EnvelopeHeaders headers, T entity, Action updater) where T : class, IEntity, new() { - EntityMapper.Update(@event, headers, entity); - - updater(entity); + entity.Update(@event, headers, updater); await collection.ReplaceOneAsync(t => t.Id == entity.Id, entity); } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs index f060b9294..e48aa1543 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs @@ -38,10 +38,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules [BsonElement] public Instant? NextAttempt { get; set; } - [BsonRequired] - [BsonElement] - public bool IsSending { get; set; } - [BsonRequired] [BsonElement] public RuleResult Result { get; set; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs index db39b6675..99b22d125 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs @@ -36,14 +36,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules protected override Task SetupCollectionAsync(IMongoCollection collection) { return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt).Descending(x => x.IsSending)), + collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt)), collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created)), collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); } public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) { - return Collection.Find(x => x.NextAttempt < now && !x.IsSending).ForEachAsync(callback, cancellationToken); + return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, cancellationToken); } public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) @@ -81,18 +81,12 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules return Collection.InsertOneIfNotExistsAsync(entity); } - public Task MarkSendingAsync(Guid jobId) - { - return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.IsSending, true)); - } - public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) { return Collection.UpdateOneAsync(x => x.Id == jobId, Update.Set(x => x.Result, result) .Set(x => x.LastDump, dump) .Set(x => x.JobResult, jobResult) - .Set(x => x.IsSending, false) .Set(x => x.NextAttempt, nextAttempt) .Inc(x => x.NumCalls, 1)); } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs deleted file mode 100644 index ab9ff0204..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// MongoRuleRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Rules -{ - public partial class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, IEventConsumer - { - private static readonly List EmptyRules = new List(); - private readonly SemaphoreSlim lockObject = new SemaphoreSlim(1); - private Dictionary> inMemoryRules; - - public MongoRuleRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_Rules"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return Task.WhenAll(collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId))); - } - - public async Task> QueryByAppAsync(Guid appId) - { - var entities = - await Collection.Find(x => x.AppId == appId) - .ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task> QueryCachedByAppAsync(Guid appId) - { - await EnsureRulesLoadedAsync(); - - return inMemoryRules.GetOrDefault(appId)?.ToList() ?? EmptyRules; - } - - private async Task EnsureRulesLoadedAsync() - { - if (inMemoryRules == null) - { - try - { - await lockObject.WaitAsync(); - - if (inMemoryRules == null) - { - inMemoryRules = new Dictionary>(); - - var webhooks = - await Collection.Find(new BsonDocument()) - .ToListAsync(); - - foreach (var webhook in webhooks) - { - inMemoryRules.GetOrAddNew(webhook.AppId).Add(webhook); - } - } - } - finally - { - lockObject.Release(); - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs deleted file mode 100644 index 26be2002a..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleRepository_EventHandling.cs +++ /dev/null @@ -1,97 +0,0 @@ -// ========================================================================== -// MongoRuleRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Domain.Apps.Events.Rules.Utils; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Read.MongoDb.Rules -{ - public partial class MongoRuleRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^rule-"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected async Task On(RuleCreated @event, EnvelopeHeaders headers) - { - await EnsureRulesLoadedAsync(); - - await Collection.CreateAsync(@event, headers, w => - { - w.Rule = RuleEventDispatcher.Create(@event); - - inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryRules.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(RuleUpdated @event, EnvelopeHeaders headers) - { - await EnsureRulesLoadedAsync(); - - await Collection.UpdateAsync(@event, headers, w => - { - w.Rule.Apply(@event); - - inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryRules.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(RuleEnabled @event, EnvelopeHeaders headers) - { - await EnsureRulesLoadedAsync(); - - await Collection.UpdateAsync(@event, headers, w => - { - w.Rule.Apply(@event); - - inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryRules.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(RuleDisabled @event, EnvelopeHeaders headers) - { - await EnsureRulesLoadedAsync(); - - await Collection.UpdateAsync(@event, headers, w => - { - w.Rule.Apply(@event); - - inMemoryRules.GetOrAddNew(w.AppId).RemoveAll(x => x.Id == w.Id); - inMemoryRules.GetOrAddNew(w.AppId).Add(w); - }); - } - - protected async Task On(RuleDeleted @event, EnvelopeHeaders headers) - { - await EnsureRulesLoadedAsync(); - - inMemoryRules.GetOrAddNew(@event.AppId.Id).RemoveAll(x => x.Id == @event.RuleId); - - await Collection.DeleteManyAsync(x => x.Id == @event.RuleId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs deleted file mode 100644 index f17434021..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// MongoSchemaRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Schemas -{ - public partial class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISchemaEventConsumer - { - private readonly FieldRegistry registry; - - public MongoSchemaRepository(IMongoDatabase database, FieldRegistry registry) - : base(database) - { - Guard.NotNull(registry, nameof(registry)); - - this.registry = registry; - } - - protected override string CollectionName() - { - return "Projections_Schemas"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.IsDeleted).Ascending(x => x.Name))); - } - - public async Task> QueryAllAsync(Guid appId) - { - var schemaEntities = - await Collection.Find(s => s.AppId == appId && !s.IsDeleted) - .ToListAsync(); - - return schemaEntities.OfType().ToList(); - } - - public async Task FindSchemaAsync(Guid appId, string name) - { - var schemaEntity = - await Collection.Find(s => s.AppId == appId && !s.IsDeleted && s.Name == name) - .FirstOrDefaultAsync(); - - return schemaEntity; - } - - public async Task FindSchemaAsync(Guid schemaId) - { - var schemaEntity = - await Collection.Find(s => s.Id == schemaId) - .FirstOrDefaultAsync(); - - return schemaEntity; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs deleted file mode 100644 index 772620b38..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ /dev/null @@ -1,188 +0,0 @@ -// ========================================================================== -// MongoSchemaRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Domain.Apps.Events.Schemas.Old; -using Squidex.Domain.Apps.Events.Schemas.Utils; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.Reflection; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Read.MongoDb.Schemas -{ - public partial class MongoSchemaRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^schema-"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(SchemaCreated @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(@event, headers, s => - { - s.SchemaDef = SchemaEventDispatcher.Create(@event, registry); - - SimpleMapper.Map(@event, s); - }); - } - - protected Task On(FieldDeleted @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldLocked @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldHidden @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldShown @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldDisabled @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldEnabled @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldUpdated @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(SchemaFieldsReordered @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(SchemaUpdated @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(SchemaPublished @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(SchemaUnpublished @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event); - }); - } - - protected Task On(FieldAdded @event, EnvelopeHeaders headers) - { - return UpdateSchemaAsync(@event, headers, s => - { - s.SchemaDef.Apply(@event, registry); - }); - } - - protected Task On(ScriptsConfigured @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, s => - { - SimpleMapper.Map(@event, s); - }); - } - - protected Task On(SchemaDeleted @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, s => - { - s.IsDeleted = true; - }); - } - - protected Task On(WebhookAdded @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, s => - { - /* NOOP */ - }); - } - - protected Task On(WebhookDeleted @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, s => - { - /* NOOP */ - }); - } - - private Task UpdateSchemaAsync(SquidexEvent @event, EnvelopeHeaders headers, Action updater) - { - return Collection.UpdateAsync(@event, headers, s => - { - updater(s); - - s.IsPublished = s.SchemaDef.IsPublished; - }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj index 3040f8ead..b8e390b69 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Squidex/Config/Identity/AuthenticationExtensions.cs b/src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs similarity index 53% rename from src/Squidex/Config/Identity/AuthenticationExtensions.cs rename to src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs index 69fe4f294..39fe7824b 100644 --- a/src/Squidex/Config/Identity/AuthenticationExtensions.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/AppEntityExtensions.cs @@ -1,22 +1,20 @@ // ========================================================================== -// AuthenticationExtensions.cs +// AppEntityExtensions.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Microsoft.AspNetCore.Builder; +using Squidex.Domain.Apps.Core; -namespace Squidex.Config.Identity +namespace Squidex.Domain.Apps.Read.Apps { - public static class AuthenticationExtensions + public static class AppEntityExtensions { - public static IApplicationBuilder UseMyAuthentication(this IApplicationBuilder app) + public static PartitionResolver PartitionResolver(this IAppEntity entity) { - app.UseAuthentication(); - - return app; + return entity.LanguagesConfig.ToResolver(); } } } diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs index 0eaba9695..df1743d69 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs @@ -6,13 +6,14 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Read.Apps { public interface IAppEntity : IEntity, IEntityWithVersion { + string Etag { get; } + string Name { get; } string PlanId { get; } @@ -24,7 +25,5 @@ namespace Squidex.Domain.Apps.Read.Apps AppContributors Contributors { get; } LanguagesConfig LanguagesConfig { get; } - - PartitionResolver PartitionResolver { get; } } } diff --git a/src/Squidex.Domain.Apps.Read/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Read/Apps/Repositories/IAppRepository.cs deleted file mode 100644 index 67fef0148..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Repositories/IAppRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// IAppRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Apps.Repositories -{ - public interface IAppRepository - { - Task> QueryAllAsync(string subjectId); - - Task FindAppAsync(Guid appId); - - Task FindAppAsync(string name); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs deleted file mode 100644 index becf14af4..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// CachingAppProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Read.Apps.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations -{ - public class CachingAppProvider : CachingProviderBase, IAppProvider, IEventConsumer - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); - private readonly IAppRepository repository; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return string.Empty; } - } - - public CachingAppProvider(IMemoryCache cache, IAppRepository repository) - : base(cache) - { - Guard.NotNull(cache, nameof(cache)); - - this.repository = repository; - } - - public async Task FindAppByIdAsync(Guid appId) - { - var cacheKey = BuildIdCacheKey(appId); - - if (!Cache.TryGetValue(cacheKey, out IAppEntity result)) - { - result = await repository.FindAppAsync(appId); - - Cache.Set(cacheKey, result, CacheDuration); - - if (result != null) - { - Cache.Set(BuildNameCacheKey(result.Name), result, CacheDuration); - } - } - - return result; - } - - public async Task FindAppByNameAsync(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var cacheKey = BuildNameCacheKey(name); - - if (!Cache.TryGetValue(cacheKey, out IAppEntity result)) - { - result = await repository.FindAppAsync(name); - - Cache.Set(cacheKey, result, CacheDuration); - - if (result != null) - { - Cache.Set(BuildIdCacheKey(result.Id), result, CacheDuration); - } - } - - return result; - } - - public Task On(Envelope @event) - { - void Remove(NamedId id) - { - var cacheKeyById = BuildIdCacheKey(id.Id); - var cacheKeyByName = BuildNameCacheKey(id.Name); - - Cache.Remove(cacheKeyById); - Cache.Remove(cacheKeyByName); - - Cache.Invalidate(cacheKeyById); - Cache.Invalidate(cacheKeyByName); - } - - if (@event.Payload is AppEvent appEvent) - { - Remove(appEvent.AppId); - } - - return TaskHelper.Done; - } - - private static string BuildNameCacheKey(string name) - { - return $"App_Ids_{name}"; - } - - private static string BuildIdCacheKey(Guid schemaId) - { - return $"App_Names_{schemaId}"; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs index 46b0e4ea9..ae175abb6 100644 --- a/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Assets/IAssetEntity.cs @@ -8,7 +8,7 @@ namespace Squidex.Domain.Apps.Read.Assets { - public interface IAssetEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + public interface IAssetEntity : IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { string MimeType { get; } diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs index 6ffea3aea..866917d8b 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentHistoryEventsCreator.cs @@ -29,14 +29,21 @@ namespace Squidex.Domain.Apps.Read.Contents "deleted content item."); AddEventMessage( - "change status of content item to {[Status]}."); + "changed status of content item to {[Status]}."); } protected override Task CreateEventCoreAsync(Envelope @event) { var channel = $"contents.{@event.Headers.AggregateId()}"; - return Task.FromResult(ForEvent(@event.Payload, channel)); + var result = ForEvent(@event.Payload, channel); + + if (@event.Payload is ContentStatusChanged contentStatusChanged) + { + result = result.AddParameter("Status", contentStatusChanged.Status); + } + + return Task.FromResult(result); } } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs index f2a8263c3..46bb4848e 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/ContentQueryService.cs @@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Contents.Edm; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; @@ -30,23 +29,23 @@ namespace Squidex.Domain.Apps.Read.Contents public sealed class ContentQueryService : IContentQueryService { private readonly IContentRepository contentRepository; - private readonly ISchemaProvider schemas; + private readonly IAppProvider appProvider; private readonly IScriptEngine scriptEngine; private readonly EdmModelBuilder modelBuilder; public ContentQueryService( IContentRepository contentRepository, - ISchemaProvider schemas, + IAppProvider appProvider, IScriptEngine scriptEngine, EdmModelBuilder modelBuilder) { Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(modelBuilder, nameof(modelBuilder)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(appProvider, nameof(appProvider)); this.contentRepository = contentRepository; - this.schemas = schemas; + this.appProvider = appProvider; this.scriptEngine = scriptEngine; this.modelBuilder = modelBuilder; } @@ -156,12 +155,12 @@ namespace Squidex.Domain.Apps.Read.Contents if (Guid.TryParse(schemaIdOrName, out var id)) { - schema = await schemas.FindSchemaByIdAsync(id); + schema = await appProvider.GetSchemaAsync(app.Name, id); } if (schema == null) { - schema = await schemas.FindSchemaByNameAsync(app.Id, schemaIdOrName); + schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); } if (schema == null) diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs index 7f3fd63ce..696d5282d 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Read.Contents.Edm { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); - return BuildEdmModel(schema.SchemaDef, app.PartitionResolver); + return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); }); return result; diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs index f20d7cf4c..17d31bb7d 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs @@ -11,65 +11,36 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Schemas.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Read.Contents.GraphQL { - public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService, IEventConsumer + public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(60); private readonly IContentQueryService contentQuery; private readonly IGraphQLUrlGenerator urlGenerator; private readonly IAssetRepository assetRepository; - private readonly ISchemaRepository schemaRepository; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^(schema-)|(apps-)"; } - } + private readonly IAppProvider appProvider; public CachingGraphQLService(IMemoryCache cache, + IAppProvider appProvider, IAssetRepository assetRepository, IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator, - ISchemaRepository schemaRepository) + IGraphQLUrlGenerator urlGenerator) : base(cache) { - Guard.NotNull(schemaRepository, nameof(schemaRepository)); + Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(contentQuery, nameof(urlGenerator)); Guard.NotNull(contentQuery, nameof(contentQuery)); + this.appProvider = appProvider; this.assetRepository = assetRepository; this.contentQuery = contentQuery; this.urlGenerator = urlGenerator; - this.schemaRepository = schemaRepository; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - - public Task On(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - Cache.Remove(CreateCacheKey(appEvent.AppId.Id)); - } - - return TaskHelper.Done; } public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query) @@ -90,13 +61,13 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL private async Task GetModelAsync(IAppEntity app) { - var cacheKey = CreateCacheKey(app.Id); + var cacheKey = CreateCacheKey(app.Id, app.Etag); var modelContext = Cache.Get(cacheKey); if (modelContext == null) { - var allSchemas = await schemaRepository.QueryAllAsync(app.Id); + var allSchemas = await appProvider.GetSchemasAsync(app.Name); modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); @@ -106,9 +77,9 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL return modelContext; } - private static object CreateCacheKey(Guid appId) + private static object CreateCacheKey(Guid appId, string etag) { - return $"GraphQLModel_{appId}"; + return $"GraphQLModel_{appId}_{etag}"; } } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs index 50c64f64f..292cc138b 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; - partitionResolver = app.PartitionResolver; + partitionResolver = app.PartitionResolver(); assetType = new AssetGraphType(this); assetListType = new ListGraphType(new NonNullGraphType(assetType)); diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs index d8ee25be8..476230a3a 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLQuery.cs @@ -6,6 +6,8 @@ // All rights reserved. // ========================================================================== +using Newtonsoft.Json.Linq; + namespace Squidex.Domain.Apps.Read.Contents.GraphQL { public class GraphQLQuery @@ -16,6 +18,6 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public string Query { get; set; } - public string Variables { get; set; } + public JObject Variables { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs index dc34ea65e..fb704d635 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/IContentEntity.cs @@ -11,7 +11,7 @@ using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Read.Contents { - public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + public interface IContentEntity : IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { Status Status { get; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs b/src/Squidex.Domain.Apps.Read/EntityMapper.cs similarity index 65% rename from src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs rename to src/Squidex.Domain.Apps.Read/EntityMapper.cs index 0557c28f4..525f94ff2 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Read/EntityMapper.cs @@ -6,15 +6,15 @@ // All rights reserved. // ========================================================================== +using System; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb +namespace Squidex.Domain.Apps.Read { public static class EntityMapper { - public static T Create(SquidexEvent @event, EnvelopeHeaders headers) where T : IMongoEntity, new() + public static T Create(SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity, new() { var entity = new T(); @@ -26,60 +26,62 @@ namespace Squidex.Domain.Apps.Read.MongoDb SetAppId(@event, entity); - return Update(@event, headers, entity); + return entity.Update(@event, headers, updater); } - public static T Update(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : IMongoEntity, new() + public static T Update(this T entity, SquidexEvent @event, EnvelopeHeaders headers, Action updater = null) where T : IEntity, new() { SetVersion(headers, entity); SetLastModified(headers, entity); SetLastModifiedBy(@event, entity); + updater?.Invoke(entity); + return entity; } - private static void SetId(EnvelopeHeaders headers, IMongoEntity entity) + private static void SetId(EnvelopeHeaders headers, IEntity entity) { entity.Id = headers.AggregateId(); } - private static void SetCreated(EnvelopeHeaders headers, IMongoEntity entity) + private static void SetCreated(EnvelopeHeaders headers, IEntity entity) { entity.Created = headers.Timestamp(); } - private static void SetLastModified(EnvelopeHeaders headers, IMongoEntity entity) + private static void SetLastModified(EnvelopeHeaders headers, IEntity entity) { entity.LastModified = headers.Timestamp(); } - private static void SetVersion(EnvelopeHeaders headers, IMongoEntity entity) + private static void SetVersion(EnvelopeHeaders headers, IEntity entity) { - if (entity is IEntityWithVersion withVersion) + if (entity is IUpdateableEntityWithVersion withVersion) { withVersion.Version = headers.EventStreamNumber(); } } - private static void SetCreatedBy(SquidexEvent @event, IMongoEntity entity) + private static void SetCreatedBy(SquidexEvent @event, IEntity entity) { - if (entity is IEntityWithCreatedBy withCreatedBy) + if (entity is IUpdateableEntityWithCreatedBy withCreatedBy) { withCreatedBy.CreatedBy = @event.Actor; } } - private static void SetLastModifiedBy(SquidexEvent @event, IMongoEntity entity) + private static void SetLastModifiedBy(SquidexEvent @event, IEntity entity) { - if (entity is IEntityWithLastModifiedBy withModifiedBy) + if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) { withModifiedBy.LastModifiedBy = @event.Actor; } } - private static void SetAppId(SquidexEvent @event, IMongoEntity entity) + private static void SetAppId(SquidexEvent @event, IEntity entity) { - if (entity is IAppRefEntity app && @event is AppEvent appEvent) + if (entity is IUpdateableEntityWithAppRef app && @event is AppEvent appEvent) { app.AppId = appEvent.AppId.Id; } diff --git a/src/Squidex.Domain.Apps.Read/IAppProvider.cs b/src/Squidex.Domain.Apps.Read/IAppProvider.cs new file mode 100644 index 000000000..296f8ff2d --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/IAppProvider.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// IApps.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Schemas; + +namespace Squidex.Domain.Apps.Read +{ + public interface IAppProvider + { + Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id); + + Task GetAppAsync(string appName); + + Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false); + + Task GetSchemaAsync(string appName, string name, bool provideDeleted = false); + + Task> GetSchemasAsync(string appName); + + Task> GetRulesAsync(string appName); + + Task> GetUserApps(string userId); + } +} diff --git a/src/Squidex.Domain.Apps.Read/IEntity.cs b/src/Squidex.Domain.Apps.Read/IEntity.cs index a93d2980a..89aea8bed 100644 --- a/src/Squidex.Domain.Apps.Read/IEntity.cs +++ b/src/Squidex.Domain.Apps.Read/IEntity.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Read { public interface IEntity { - Guid Id { get; } + Guid Id { get; set; } - Instant Created { get; } + Instant Created { get; set; } - Instant LastModified { get; } + Instant LastModified { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs b/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs new file mode 100644 index 000000000..1070538d1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/IEntityWithAppRef.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IEntityWithAppRef.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Read +{ + public interface IEntityWithAppRef : IEntity + { + Guid AppId { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs b/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs index e06701152..4661a15ee 100644 --- a/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs +++ b/src/Squidex.Domain.Apps.Read/IEntityWithCreatedBy.cs @@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Read { public interface IEntityWithCreatedBy { - RefToken CreatedBy { get; set; } + RefToken CreatedBy { get; } } } diff --git a/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs b/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs index b80ed6b08..860918deb 100644 --- a/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs +++ b/src/Squidex.Domain.Apps.Read/IEntityWithVersion.cs @@ -10,6 +10,6 @@ namespace Squidex.Domain.Apps.Read { public interface IEntityWithVersion { - long Version { get; set; } + long Version { get; } } } diff --git a/src/Squidex.Domain.Apps.Read/IAppRefEntity.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs similarity index 82% rename from src/Squidex.Domain.Apps.Read/IAppRefEntity.cs rename to src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs index f92f52ffc..316962a96 100644 --- a/src/Squidex.Domain.Apps.Read/IAppRefEntity.cs +++ b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithAppRef.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IAppRefEntity.cs +// IUpdateableEntityWithAppRef.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,8 +10,8 @@ using System; namespace Squidex.Domain.Apps.Read { - public interface IAppRefEntity : IEntity + public interface IUpdateableEntityWithAppRef { Guid AppId { get; set; } } -} \ No newline at end of file +} diff --git a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs similarity index 63% rename from src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs rename to src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs index f94bdee68..3675937c4 100644 --- a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs +++ b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithCreatedBy.cs @@ -1,16 +1,17 @@ // ========================================================================== -// ISchemaEventConsumer.cs +// IUpdateableEntityWithCreatedBy.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Read.Schemas +namespace Squidex.Domain.Apps.Read { - public interface ISchemaEventConsumer : IEventConsumer + public interface IUpdateableEntityWithCreatedBy { + RefToken CreatedBy { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs new file mode 100644 index 000000000..d1aedc9f4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithLastModifiedBy.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IUpdateableEntityWithLastModifiedBy.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read +{ + public interface IUpdateableEntityWithLastModifiedBy + { + RefToken LastModifiedBy { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs new file mode 100644 index 000000000..8d08b4c6a --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/IUpdateableEntityWithVersion.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IUpdateableEntityWithVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Read +{ + public interface IUpdateableEntityWithVersion + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs index 6de2db9dd..424febe38 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/IRuleEntity.cs @@ -10,8 +10,8 @@ using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Domain.Apps.Read.Rules { - public interface IRuleEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + public interface IRuleEntity : IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { - Rule Rule { get; } + Rule RuleDef { get; } } } diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs index 256aa9b71..2b05e6741 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleEventRepository.cs @@ -22,8 +22,6 @@ namespace Squidex.Domain.Apps.Read.Rules.Repositories Task EnqueueAsync(Guid id, Instant nextAttempt); - Task MarkSendingAsync(Guid jobId); - Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextCall); Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)); diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs index 3f6f6e7c6..b738dfeef 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleDequeuer.cs @@ -7,11 +7,13 @@ // ========================================================================== using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -22,10 +24,10 @@ namespace Squidex.Domain.Apps.Read.Rules public sealed class RuleDequeuer : DisposableObjectBase, IExternalSystem { private readonly ActionBlock requestBlock; - private readonly TransformBlock blockBlock; private readonly IRuleEventRepository ruleEventRepository; private readonly RuleService ruleService; private readonly CompletionTimer timer; + private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); private readonly IClock clock; private readonly ISemanticLog log; @@ -44,15 +46,9 @@ namespace Squidex.Domain.Apps.Read.Rules this.log = log; requestBlock = - new ActionBlock(MakeRequestAsync, + new ActionBlock(HandleAsync, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - blockBlock = - new TransformBlock(x => BlockAsync(x), - new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1, BoundedCapacity = 1 }); - - blockBlock.LinkTo(requestBlock, new DataflowLinkOptions { PropagateCompletion = true }); - timer = new CompletionTimer(5000, QueryAsync); } @@ -62,7 +58,7 @@ namespace Squidex.Domain.Apps.Read.Rules { timer.StopAsync().Wait(); - blockBlock.Complete(); + requestBlock.Complete(); requestBlock.Completion.Wait(); } } @@ -82,7 +78,7 @@ namespace Squidex.Domain.Apps.Read.Rules { var now = clock.GetCurrentInstant(); - await ruleEventRepository.QueryPendingAsync(now, blockBlock.SendAsync, cancellationToken); + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, cancellationToken); } catch (Exception ex) { @@ -92,78 +88,70 @@ namespace Squidex.Domain.Apps.Read.Rules } } - private async Task BlockAsync(IRuleEventEntity @event) + public async Task HandleAsync(IRuleEventEntity @event) { - try + if (!executing.TryAdd(@event.Id, false)) { - await ruleEventRepository.MarkSendingAsync(@event.Id); - - return @event; + return; } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "BlockWebhookEvent") - .WriteProperty("status", "Failed")); - - throw; - } - } - private async Task MakeRequestAsync(IRuleEventEntity @event) - { try { var job = @event.Job; var response = await ruleService.InvokeAsync(job.ActionName, job.ActionData); - Instant? nextCall = null; - - if (response.Result != RuleResult.Success) - { - switch (@event.NumCalls) - { - case 0: - nextCall = job.Created.Plus(Duration.FromMinutes(5)); - break; - case 1: - nextCall = job.Created.Plus(Duration.FromHours(1)); - break; - case 2: - nextCall = job.Created.Plus(Duration.FromHours(6)); - break; - case 3: - nextCall = job.Created.Plus(Duration.FromHours(12)); - break; - } - } - - RuleJobResult jobResult; + var jobInvoke = ComputeJobInvoke(response.Result, @event, job); + var jobResult = ComputeJobResult(response.Result, jobInvoke); - if (response.Result != RuleResult.Success && !nextCall.HasValue) - { - jobResult = RuleJobResult.Failed; - } - else if (response.Result != RuleResult.Success && nextCall.HasValue) - { - jobResult = RuleJobResult.Retry; - } - else - { - jobResult = RuleJobResult.Success; - } - - await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, nextCall); + await ruleEventRepository.MarkSentAsync(@event.Id, response.Dump, response.Result, jobResult, response.Elapsed, jobInvoke); } catch (Exception ex) { log.LogError(ex, w => w .WriteProperty("action", "SendWebhookEvent") .WriteProperty("status", "Failed")); + } + finally + { + executing.TryRemove(@event.Id, out var value); + } + } + + private static RuleJobResult ComputeJobResult(RuleResult result, Instant? nextCall) + { + if (result != RuleResult.Success && !nextCall.HasValue) + { + return RuleJobResult.Failed; + } + else if (result != RuleResult.Success && nextCall.HasValue) + { + return RuleJobResult.Retry; + } + else + { + return RuleJobResult.Success; + } + } - throw; + private static Instant? ComputeJobInvoke(RuleResult result, IRuleEventEntity @event, RuleJob job) + { + if (result != RuleResult.Success) + { + switch (@event.NumCalls) + { + case 0: + return job.Created.Plus(Duration.FromMinutes(5)); + case 1: + return job.Created.Plus(Duration.FromHours(1)); + case 2: + return job.Created.Plus(Duration.FromHours(6)); + case 3: + return job.Created.Plus(Duration.FromHours(12)); + } } + + return null; } } } diff --git a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs index 01870edb7..5a3a492d4 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs +++ b/src/Squidex.Domain.Apps.Read/Rules/RuleEnqueuer.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Read.Rules public sealed class RuleEnqueuer : IEventConsumer { private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleRepository ruleRepository; + private readonly IAppProvider appProvider; private readonly RuleService ruleService; public string Name @@ -33,17 +33,18 @@ namespace Squidex.Domain.Apps.Read.Rules } public RuleEnqueuer( - IRuleEventRepository ruleEventRepository, - IRuleRepository ruleRepository, + IRuleEventRepository ruleEventRepository, IAppProvider appProvider, RuleService ruleService) { Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); - Guard.NotNull(ruleRepository, nameof(ruleRepository)); Guard.NotNull(ruleService, nameof(ruleService)); + Guard.NotNull(appProvider, nameof(appProvider)); + this.ruleEventRepository = ruleEventRepository; - this.ruleRepository = ruleRepository; this.ruleService = ruleService; + + this.appProvider = appProvider; } public Task ClearAsync() @@ -55,11 +56,11 @@ namespace Squidex.Domain.Apps.Read.Rules { if (@event.Payload is AppEvent appEvent) { - var rules = await ruleRepository.QueryCachedByAppAsync(appEvent.AppId.Id); + var rules = await appProvider.GetRulesAsync(appEvent.AppId.Name); foreach (var ruleEntity in rules) { - var job = ruleService.CreateJob(ruleEntity.Rule, @event); + var job = ruleService.CreateJob(ruleEntity.RuleDef, @event); if (job != null) { diff --git a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs index 17348a1b4..90caafd74 100644 --- a/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Read.Schemas { - public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion + public interface ISchemaEntity : IEntityWithAppRef, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaRepository.cs deleted file mode 100644 index 147e4d1a9..000000000 --- a/src/Squidex.Domain.Apps.Read/Schemas/Repositories/ISchemaRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// ISchemaRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Schemas.Repositories -{ - public interface ISchemaRepository - { - Task> QueryAllAsync(Guid appId); - - Task FindSchemaAsync(Guid appId, string name); - - Task FindSchemaAsync(Guid schemaId); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Schemas/Services/ISchemaProvider.cs b/src/Squidex.Domain.Apps.Read/Schemas/Services/ISchemaProvider.cs deleted file mode 100644 index 6d50efe1a..000000000 --- a/src/Squidex.Domain.Apps.Read/Schemas/Services/ISchemaProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -// ========================================================================== -// ISchemaProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Domain.Apps.Read.Schemas.Services -{ - public interface ISchemaProvider - { - Task FindSchemaByIdAsync(Guid id, bool provideDeleted = false); - - Task FindSchemaByNameAsync(Guid appId, string name); - } -} diff --git a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs deleted file mode 100644 index ab0a609bd..000000000 --- a/src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// CachingSchemaProvider.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Read.Schemas.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Domain.Apps.Read.Schemas.Services.Implementations -{ - public class CachingSchemaProvider : CachingProviderBase, ISchemaProvider, IEventConsumer - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly ISchemaRepository repository; - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return string.Empty; } - } - - public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) - : base(cache) - { - Guard.NotNull(repository, nameof(repository)); - - this.repository = repository; - } - - public async Task FindSchemaByIdAsync(Guid id, bool provideDeleted = false) - { - var cacheKey = BuildIdCacheKey(id); - - if (!Cache.TryGetValue(cacheKey, out ISchemaEntity result)) - { - result = await repository.FindSchemaAsync(id); - - Cache.Set(cacheKey, result, CacheDuration); - - if (result != null) - { - Cache.Set(BuildNameCacheKey(result.AppId, result.Name), result, CacheDuration); - } - } - - if (result != null && result.IsDeleted && !provideDeleted) - { - result = null; - } - - return result; - } - - public async Task FindSchemaByNameAsync(Guid appId, string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - var cacheKey = BuildNameCacheKey(appId, name); - - if (!Cache.TryGetValue(cacheKey, out ISchemaEntity result)) - { - result = await repository.FindSchemaAsync(appId, name); - - Cache.Set(cacheKey, result, CacheDuration); - - if (result != null) - { - Cache.Set(BuildIdCacheKey(result.Id), result, CacheDuration); - } - } - - if (result != null && result.IsDeleted) - { - result = null; - } - - return result; - } - - public Task On(Envelope @event) - { - void Remove(NamedId appId, NamedId schemaId) - { - var cacheKeyById = BuildIdCacheKey(schemaId.Id); - var cacheKeyByName = BuildNameCacheKey(appId.Id, schemaId.Name); - - Cache.Remove(cacheKeyById); - Cache.Remove(cacheKeyByName); - - Cache.Invalidate(cacheKeyById); - Cache.Invalidate(cacheKeyByName); - } - - if (@event.Payload is SchemaEvent schemaEvent) - { - Remove(schemaEvent.AppId, schemaEvent.SchemaId); - } - - return TaskHelper.Done; - } - - private static string BuildNameCacheKey(Guid appId, string name) - { - return $"Schema_Ids_{appId}_{name}"; - } - - private static string BuildIdCacheKey(Guid schemaId) - { - return $"Schema_Names_{schemaId}"; - } - - public Task ClearAsync() - { - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj b/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj index 17670622a..58922eba3 100644 --- a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj +++ b/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/Squidex.Domain.Apps.Read/State/AppProvider.cs b/src/Squidex.Domain.Apps.Read/State/AppProvider.cs new file mode 100644 index 000000000..935d0708e --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/AppProvider.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// AppProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.State.Orleans.Grains; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Read.State.Orleans +{ + public sealed class AppProvider : IAppProvider + { + private readonly IStateFactory factory; + + public AppProvider(IStateFactory factory) + { + Guard.NotNull(factory, nameof(factory)); + + this.factory = factory; + } + + public async Task GetAppAsync(string appName) + { + var app = await factory.GetAsync(appName); + + return await app.GetAppAsync(); + } + + public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) + { + var app = await factory.GetAsync(appName); + + return await app.GetAppWithSchemaAsync(id); + } + + public async Task> GetRulesAsync(string appName) + { + var app = await factory.GetAsync(appName); + + return await app.GetRulesAsync(); + } + + public async Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) + { + var app = await factory.GetAsync(appName); + + return await app.GetSchemaAsync(id, provideDeleted); + } + + public async Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) + { + var app = await factory.GetAsync(appName); + + return await app.GetSchemaAsync(name, provideDeleted); + } + + public async Task> GetSchemasAsync(string appName) + { + var app = await factory.GetAsync(appName); + + return await app.GetSchemasAsync(); + } + + public async Task> GetUserApps(string userId) + { + var appUser = await factory.GetAsync(userId); + var appNames = await appUser.GetAppNamesAsync(); + + var tasks = + appNames + .Select(x => GetAppAsync(x)); + + var apps = await Task.WhenAll(tasks); + + return apps.Where(a => a != null).ToList(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs b/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs new file mode 100644 index 000000000..48e4c1678 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/AppStateEventConsumer.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// AppStateEventConsumer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Read.State.Orleans.Grains; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Read.State.Orleans +{ + public sealed class AppStateEventConsumer : IEventConsumer + { + private readonly IStateFactory factory; + + public string Name + { + get { return typeof(AppStateEventConsumer).Name; } + } + + public string EventsFilter + { + get { return @"(^app-)|(^schema-)|(^rule\-)"; } + } + + public AppStateEventConsumer(IStateFactory factory) + { + Guard.NotNull(factory, nameof(factory)); + + this.factory = factory; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppEvent appEvent) + { + var appGrain = await factory.GetAsync(appEvent.AppId.Name); + + await appGrain.HandleAsync(@event); + } + + if (@event.Payload is AppContributorAssigned contributorAssigned) + { + var userGrain = await factory.GetAsync(contributorAssigned.ContributorId); + + await userGrain.AddAppAsync(contributorAssigned.AppId.Name); + } + + if (@event.Payload is AppContributorRemoved contributorRemoved) + { + var userGrain = await factory.GetAsync(contributorRemoved.ContributorId); + + await userGrain.RemoveAppAsync(contributorRemoved.AppId.Name); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs new file mode 100644 index 000000000..64bd1b54f --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// AppStateGrain.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public class AppStateGrain : StatefulObject + { + private readonly FieldRegistry fieldRegistry; + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); + private Exception exception; + + public AppStateGrain(FieldRegistry fieldRegistry) + { + Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); + + this.fieldRegistry = fieldRegistry; + } + + public override async Task ReadStateAsync() + { + try + { + await base.ReadStateAsync(); + } + catch (Exception ex) + { + exception = ex; + + State = new AppStateGrainState(); + } + + State.SetRegistry(fieldRegistry); + } + + public virtual Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid id) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var schema = State.FindSchema(x => x.Id == id && !x.IsDeleted); + + return Task.FromResult((State.GetApp(), schema)); + }); + } + + public virtual Task GetAppAsync() + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var value = State.GetApp(); + + return Task.FromResult(value); + }); + } + + public virtual Task> GetRulesAsync() + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var value = State.FindRules(); + + return Task.FromResult(value); + }); + } + + public virtual Task> GetSchemasAsync() + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var value = State.FindSchemas(x => !x.IsDeleted); + + return Task.FromResult(value); + }); + } + + public virtual Task GetSchemaAsync(Guid id, bool provideDeleted = false) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var value = State.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); + + return Task.FromResult(value); + }); + } + + public virtual Task GetSchemaAsync(string name, bool provideDeleted = false) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + var value = State.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); + + return Task.FromResult(value); + }); + } + + public virtual Task HandleAsync(Envelope message) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + if (exception != null) + { + if (message.Payload is AppCreated) + { + exception = null; + } + else + { + throw exception; + } + } + + State.Apply(message); + + return WriteStateAsync(); + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs new file mode 100644 index 000000000..3d777f420 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// AppStateGrainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed partial class AppStateGrainState + { + private FieldRegistry registry; + + [JsonProperty] + public JsonAppEntity App { get; set; } + + [JsonProperty] + public Dictionary Rules { get; set; } + + [JsonProperty] + public Dictionary Schemas { get; set; } + + public void SetRegistry(FieldRegistry registry) + { + this.registry = registry; + } + + public IAppEntity GetApp() + { + return App; + } + + public ISchemaEntity FindSchema(Func filter) + { + return Schemas?.Values.FirstOrDefault(filter); + } + + public List FindSchemas(Func filter) + { + return Schemas?.Values.Where(filter).OfType().ToList() ?? new List(); + } + + public List FindRules() + { + return Rules?.Values.OfType().ToList() ?? new List(); + } + + public void Reset() + { + Rules = new Dictionary(); + + Schemas = new Dictionary(); + } + + public void Apply(Envelope envelope) + { + this.DispatchAction(envelope.Payload, envelope.Headers); + + if (App != null) + { + App.Etag = Guid.NewGuid().ToString(); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs new file mode 100644 index 000000000..701f85cb6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// AppStateGrainState_Apps.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Apps.Utils; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed partial class AppStateGrainState + { + public void On(AppCreated @event, EnvelopeHeaders headers) + { + Reset(); + + App = EntityMapper.Create(@event, headers, a => + { + SimpleMapper.Map(@event, a); + + a.LanguagesConfig = LanguagesConfig.Build(Language.EN); + }); + } + + public void On(AppLanguageAdded @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.LanguagesConfig = a.LanguagesConfig.Apply(@event); + }); + } + + public void On(AppLanguageRemoved @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.LanguagesConfig = a.LanguagesConfig.Apply(@event); + }); + } + + public void On(AppLanguageUpdated @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.LanguagesConfig = a.LanguagesConfig.Apply(@event); + }); + } + + public void On(AppContributorAssigned @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Contributors = a.Contributors.Apply(@event); + }); + } + + public void On(AppContributorRemoved @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Contributors = a.Contributors.Apply(@event); + }); + } + + public void On(AppClientAttached @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Clients = a.Clients.Apply(@event); + }); + } + + public void On(AppClientUpdated @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Clients = a.Clients.Apply(@event); + }); + } + + public void On(AppClientRenamed @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Clients = a.Clients.Apply(@event); + }); + } + + public void On(AppClientRevoked @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + a.Clients = a.Clients.Apply(@event); + }); + } + + public void On(AppPlanChanged @event, EnvelopeHeaders headers) + { + UpdateApp(@event, headers, a => + { + SimpleMapper.Map(@event, a); + }); + } + + private void UpdateApp(AppEvent @event, EnvelopeHeaders headers, Action updater = null) + { + App = App.Clone().Update(@event, headers, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs new file mode 100644 index 000000000..53f0e9a82 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// AppStateGrainState_Rules.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed partial class AppStateGrainState + { + public void On(RuleCreated @event, EnvelopeHeaders headers) + { + Rules[@event.RuleId] = EntityMapper.Create(@event, headers, r => + { + r.RuleDef = RuleEventDispatcher.Create(@event); + }); + } + + public void On(RuleUpdated @event, EnvelopeHeaders headers) + { + UpdateRule(@event, headers, r => + { + r.RuleDef = r.RuleDef.Apply(@event); + }); + } + + public void On(RuleEnabled @event, EnvelopeHeaders headers) + { + UpdateRule(@event, headers, r => + { + r.RuleDef = r.RuleDef.Apply(@event); + }); + } + + public void On(RuleDisabled @event, EnvelopeHeaders headers) + { + UpdateRule(@event, headers, r => + { + r.RuleDef = r.RuleDef.Apply(@event); + }); + } + + public void On(RuleDeleted @event, EnvelopeHeaders headers) + { + Rules.Remove(@event.RuleId); + } + + private void UpdateRule(RuleEvent @event, EnvelopeHeaders headers, Action updater = null) + { + var id = @event.RuleId; + + Rules[id] = Rules[id].Clone().Update(@event, headers, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs new file mode 100644 index 000000000..dfb772b9d --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// AppStateGrainState_Schemas.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Domain.Apps.Events.Schemas.Old; +using Squidex.Domain.Apps.Events.Schemas.Utils; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed partial class AppStateGrainState + { + public void On(SchemaCreated @event, EnvelopeHeaders headers) + { + Schemas[@event.SchemaId.Id] = EntityMapper.Create(@event, headers, s => + { + s.SchemaDef = SchemaEventDispatcher.Create(@event, registry); + + SimpleMapper.Map(@event, s); + }); + } + + public void On(SchemaPublished @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(SchemaUnpublished @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(ScriptsConfigured @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + SimpleMapper.Map(s, @event); + }); + } + + public void On(SchemaUpdated @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(SchemaFieldsReordered @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(SchemaDeleted @event) + { + Schemas.Remove(@event.SchemaId.Id); + } + + public void On(FieldAdded @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event, registry); + }); + } + + public void On(FieldUpdated @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldLocked @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldDisabled @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldEnabled @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldHidden @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldShown @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(FieldDeleted @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers, s => + { + s.SchemaDef = s.SchemaDef.Apply(@event); + }); + } + + public void On(WebhookAdded @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers); + } + + public void On(WebhookDeleted @event, EnvelopeHeaders headers) + { + UpdateSchema(@event, headers); + } + + private void UpdateSchema(SchemaEvent @event, EnvelopeHeaders headers, Action updater = null) + { + var id = @event.SchemaId.Id; + + Schemas[id] = Schemas[id].Clone().Update(@event, headers, updater); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs new file mode 100644 index 000000000..9748c65e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// AppUserGrain.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed class AppUserGrain : StatefulObject + { + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); + + public Task AddAppAsync(string appName) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + State.AppNames.Add(appName); + + return WriteStateAsync(); + }); + } + + public Task RemoveAppAsync(string appName) + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + State.AppNames.Remove(appName); + + return WriteStateAsync(); + }); + } + + public Task> GetAppNamesAsync() + { + return dispatcher.DispatchAndUnwrapAsync(() => + { + return Task.FromResult(State.AppNames.ToList()); + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs similarity index 54% rename from src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs rename to src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs index 5f24b1a4e..216b6c51d 100644 --- a/src/Squidex.Domain.Apps.Read/Rules/Repositories/IRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs @@ -1,21 +1,19 @@ // ========================================================================== -// IRuleRepository.cs +// AppUserGrainState.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; -using System.Threading.Tasks; +using Newtonsoft.Json; -namespace Squidex.Domain.Apps.Read.Rules.Repositories +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains { - public interface IRuleRepository + public sealed class AppUserGrainState { - Task> QueryByAppAsync(Guid appId); - - Task> QueryCachedByAppAsync(Guid appId); + [JsonProperty] + public HashSet AppNames { get; set; } = new HashSet(); } } diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs new file mode 100644 index 000000000..306a9a05e --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/JsonAppEntity.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// JsonAppEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read.Apps; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public sealed class JsonAppEntity : JsonEntity, IAppEntity + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string PlanId { get; set; } + + [JsonProperty] + public string Etag { get; set; } + + [JsonProperty] + public string PlanOwner { get; set; } + + [JsonProperty] + public AppClients Clients { get; set; } = AppClients.Empty; + + [JsonProperty] + public AppContributors Contributors { get; set; } = AppContributors.Empty; + + [JsonProperty] + public LanguagesConfig LanguagesConfig { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs new file mode 100644 index 000000000..0cf3ba63b --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/State/Grains/JsonEntity.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// JsonEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains +{ + public abstract class JsonEntity : Cloneable, IUpdateableEntityWithVersion where T : Cloneable + { + [JsonProperty] + public Guid Id { get; set; } + + [JsonProperty] + public Instant Created { get; set; } + + [JsonProperty] + public Instant LastModified { get; set; } + + [JsonProperty] + public long Version { get; set; } + + public T Clone() + { + return Clone(x => { }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs similarity index 52% rename from src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs rename to src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs index be9369804..3ddf988c4 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/JsonRuleEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoRuleEntity.cs +// JsonRuleEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,35 +7,30 @@ // ========================================================================== using System; -using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Read.Rules; using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Rules +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains { - public class MongoRuleEntity : MongoEntity, IRuleEntity + public sealed class JsonRuleEntity : + JsonEntity, + IRuleEntity, + IUpdateableEntityWithAppRef, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy { - [BsonRequired] - [BsonElement] + [JsonProperty] public Guid AppId { get; set; } - [BsonRequired] - [BsonElement] + [JsonProperty] public RefToken CreatedBy { get; set; } - [BsonRequired] - [BsonElement] + [JsonProperty] public RefToken LastModifiedBy { get; set; } - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - [BsonJson] - public Rule Rule { get; set; } + [JsonProperty] + public Rule RuleDef { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs similarity index 50% rename from src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs rename to src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs index 7efc0059b..346a59054 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/JsonSchemaEntity.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoSchemaEntity.cs +// JsonSchemaEntity.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,68 +7,57 @@ // ========================================================================== using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Schemas +namespace Squidex.Domain.Apps.Read.State.Orleans.Grains { - public sealed class MongoSchemaEntity : MongoEntity, ISchemaEntity + public sealed class JsonSchemaEntity : + JsonEntity, + ISchemaEntity, + IUpdateableEntityWithAppRef, + IUpdateableEntityWithCreatedBy, + IUpdateableEntityWithLastModifiedBy { - [BsonRequired] - [BsonElement] + [JsonProperty] public string Name { get; set; } - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] + [JsonProperty] public Guid AppId { get; set; } - [BsonRequired] - [BsonElement] + [JsonProperty] public RefToken CreatedBy { get; set; } - [BsonRequired] - [BsonElement] + [JsonProperty] public RefToken LastModifiedBy { get; set; } - [BsonRequired] - [BsonElement] - public bool IsPublished { get; set; } - - [BsonRequired] - [BsonElement] + [JsonProperty] public bool IsDeleted { get; set; } - [BsonIgnoreIfNull] - [BsonElement] + [JsonProperty] public string ScriptQuery { get; set; } - [BsonIgnoreIfNull] - [BsonElement] + [JsonProperty] public string ScriptCreate { get; set; } - [BsonIgnoreIfNull] - [BsonElement] + [JsonProperty] public string ScriptUpdate { get; set; } - [BsonIgnoreIfNull] - [BsonElement] + [JsonProperty] public string ScriptDelete { get; set; } - [BsonIgnoreIfNull] - [BsonElement] + [JsonProperty] public string ScriptChange { get; set; } - [BsonRequired] - [BsonElement] - [BsonJson] + [JsonProperty] public Schema SchemaDef { get; set; } + + [JsonIgnore] + public bool IsPublished + { + get { return SchemaDef.IsPublished; } + } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs index 03f5957bd..02042a262 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Domain.Apps.Write.Apps.Guards; diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs index d891c619a..ce675b1e6 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs @@ -22,10 +22,9 @@ namespace Squidex.Domain.Apps.Write.Apps { public class AppDomainObject : DomainObjectBase { - private static readonly Language DefaultLanguage = Language.EN; - private readonly AppContributors contributors = new AppContributors(); - private readonly AppClients clients = new AppClients(); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(DefaultLanguage); + private AppContributors contributors = AppContributors.Empty; + private AppClients clients = AppClients.Empty; + private LanguagesConfig languagesConfig = LanguagesConfig.English; private AppPlan plan; private string name; @@ -66,47 +65,47 @@ namespace Squidex.Domain.Apps.Write.Apps protected void On(AppContributorAssigned @event) { - contributors.Apply(@event); + contributors = contributors.Apply(@event); } protected void On(AppContributorRemoved @event) { - contributors.Apply(@event); + contributors = contributors.Apply(@event); } protected void On(AppClientAttached @event) { - clients.Apply(@event); + clients = clients.Apply(@event); } protected void On(AppClientUpdated @event) { - clients.Apply(@event); + clients = clients.Apply(@event); } protected void On(AppClientRenamed @event) { - clients.Apply(@event); + clients = clients.Apply(@event); } protected void On(AppClientRevoked @event) { - clients.Apply(@event); + clients = clients.Apply(@event); } protected void On(AppLanguageAdded @event) { - languagesConfig.Apply(@event); + languagesConfig = languagesConfig.Apply(@event); } protected void On(AppLanguageRemoved @event) { - languagesConfig.Apply(@event); + languagesConfig = languagesConfig.Apply(@event); } protected void On(AppLanguageUpdated @event) { - languagesConfig.Apply(@event); + languagesConfig = languagesConfig.Apply(@event); } protected void On(AppPlanChanged @event) @@ -234,7 +233,7 @@ namespace Squidex.Domain.Apps.Write.Apps private static AppLanguageAdded CreateInitialLanguage(NamedId id) { - return new AppLanguageAdded { AppId = id, Language = DefaultLanguage }; + return new AppLanguageAdded { AppId = id, Language = Language.EN }; } private static AppContributorAssigned CreateInitialOwner(NamedId id, SquidexCommand command) diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs index c87e8ffda..89540ff13 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; @@ -17,13 +18,13 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { public static class GuardApp { - public static Task CanCreate(CreateApp command, IAppProvider apps) + public static Task CanCreate(CreateApp command, IAppProvider appProvider) { Guard.NotNull(command, nameof(command)); return Validate.It(() => "Cannot create app.", async error => { - if (await apps.FindAppByNameAsync(command.Name) != null) + if (await appProvider.GetAppAsync(command.Name) != null) { error(new ValidationError($"An app with name '{command.Name}' already exists", nameof(command.Name))); } diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index d64143b2f..e4ecb773c 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -9,10 +9,9 @@ using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Domain.Apps.Write.Contents.Guards; using Squidex.Infrastructure; @@ -27,26 +26,22 @@ namespace Squidex.Domain.Apps.Write.Contents private readonly IAppProvider appProvider; private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; - private readonly ISchemaProvider schemas; private readonly IScriptEngine scriptEngine; public ContentCommandMiddleware( IAggregateHandler handler, IAppProvider appProvider, IAssetRepository assetRepository, - ISchemaProvider schemas, IScriptEngine scriptEngine, IContentRepository contentRepository) { Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(schemas, nameof(schemas)); Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(contentRepository, nameof(contentRepository)); this.handler = handler; - this.schemas = schemas; this.appProvider = appProvider; this.scriptEngine = scriptEngine; this.assetRepository = assetRepository; @@ -154,9 +149,8 @@ namespace Squidex.Domain.Apps.Write.Contents content, command, appProvider, - schemas, - scriptEngine, assetRepository, + scriptEngine, message); return operationContext; diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs index 72a4ad097..0d1ab8077 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs @@ -13,16 +13,17 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; +#pragma warning disable IDE0017 // Simplify object initialization + namespace Squidex.Domain.Apps.Write.Contents { public sealed class ContentOperationContext @@ -41,25 +42,21 @@ namespace Squidex.Domain.Apps.Write.Contents ContentDomainObject content, ContentCommand command, IAppProvider appProvider, - ISchemaProvider schemas, - IScriptEngine scriptEngine, IAssetRepository assetRepository, + IScriptEngine scriptEngine, Func message) { - var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); - var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); - - await Task.WhenAll(taskForApp, taskForSchema); + var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Name, command.SchemaId.Id); var context = new ContentOperationContext(); - context.appEntity = taskForApp.Result; + context.appEntity = appEntity; context.assetRepository = assetRepository; context.contentRepository = contentRepository; context.content = content; context.command = command; context.message = message; - context.schemaEntity = taskForSchema.Result; + context.schemaEntity = schemaEntity; context.scriptEngine = scriptEngine; return context; @@ -69,7 +66,7 @@ namespace Squidex.Domain.Apps.Write.Contents { if (command is ContentDataCommand dataCommand) { - dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver); + dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); } return TaskHelper.Done; @@ -96,11 +93,11 @@ namespace Squidex.Domain.Apps.Write.Contents if (partial) { - await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver, errors); + await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); } else { - await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver, errors); + await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); } if (errors.Count > 0) diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs index 47a6cc129..5848f9f1e 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/GuardRule.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Infrastructure; @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Write.Rules.Guards { public static class GuardRule { - public static Task CanCreate(CreateRule command, ISchemaProvider schemas) + public static Task CanCreate(CreateRule command, IAppProvider appProvider) { Guard.NotNull(command, nameof(command)); @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Write.Rules.Guards } else { - var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); errors.Foreach(error); } @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Write.Rules.Guards }); } - public static Task CanUpdate(UpdateRule command, ISchemaProvider schemas) + public static Task CanUpdate(UpdateRule command, IAppProvider appProvider) { Guard.NotNull(command, nameof(command)); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Write.Rules.Guards if (command.Trigger != null) { - var errors = await RuleTriggerValidator.ValidateAsync(command.Trigger, schemas); + var errors = await RuleTriggerValidator.ValidateAsync(command.AppId.Name, command.Trigger, appProvider); errors.Foreach(error); } diff --git a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs index f9594c26f..48dfec259 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/Guards/RuleTriggerValidator.cs @@ -6,31 +6,33 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Read; +using Squidex.Domain.Apps.Read.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Rules.Guards { public sealed class RuleTriggerValidator : IRuleTriggerVisitor>> { - public ISchemaProvider Schemas { get; } + public Func> SchemaProvider { get; } - public RuleTriggerValidator(ISchemaProvider schemas) + public RuleTriggerValidator(Func> schemaProvider) { - Schemas = schemas; + SchemaProvider = schemaProvider; } - public static Task> ValidateAsync(RuleTrigger action, ISchemaProvider schemas) + public static Task> ValidateAsync(string appName, RuleTrigger action, IAppProvider appProvider) { Guard.NotNull(action, nameof(action)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(appProvider, nameof(appProvider)); - var visitor = new RuleTriggerValidator(schemas); + var visitor = new RuleTriggerValidator(x => appProvider.GetSchemaAsync(appName, x)); return action.Accept(visitor); } @@ -41,7 +43,7 @@ namespace Squidex.Domain.Apps.Write.Rules.Guards { var schemaErrors = await Task.WhenAll( trigger.Schemas.Select(async s => - await Schemas.FindSchemaByIdAsync(s.SchemaId) == null + await SchemaProvider(s.SchemaId) == null ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) : null)); diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs index b0b2ed082..7a29bd301 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleCommandMiddleware.cs @@ -8,7 +8,7 @@ using System; using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Domain.Apps.Write.Rules.Guards; using Squidex.Infrastructure; @@ -20,22 +20,23 @@ namespace Squidex.Domain.Apps.Write.Rules public class RuleCommandMiddleware : ICommandMiddleware { private readonly IAggregateHandler handler; - private readonly ISchemaProvider schemas; + private readonly IAppProvider appProvider; - public RuleCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) + public RuleCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) { Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(appProvider, nameof(appProvider)); this.handler = handler; - this.schemas = schemas; + + this.appProvider = appProvider; } protected Task On(CreateRule command, CommandContext context) { return handler.CreateAsync(context, async w => { - await GuardRule.CanCreate(command, schemas); + await GuardRule.CanCreate(command, appProvider); w.Create(command); }); @@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Write.Rules { return handler.UpdateAsync(context, async c => { - await GuardRule.CanUpdate(command, schemas); + await GuardRule.CanUpdate(command, appProvider); c.Update(command); }); diff --git a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs index ccf2e4a57..edf8c63d2 100644 --- a/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Rules/RuleDomainObject.cs @@ -41,17 +41,17 @@ namespace Squidex.Domain.Apps.Write.Rules protected void On(RuleUpdated @event) { - rule.Apply(@event); + rule = rule.Apply(@event); } protected void On(RuleEnabled @event) { - rule.Apply(@event); + rule = rule.Apply(@event); } protected void On(RuleDisabled @event) { - rule.Apply(@event); + rule = rule.Apply(@event); } protected void On(RuleDeleted @event) diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs index 04b10fe84..a52793ab7 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Length == 0)) + if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) { yield return new ValidationError("Radio buttons or dropdown list need allowed values.", nameof(properties.AllowedValues)); @@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.MaxValue)); } - if (properties.AllowedValues != null && properties.AllowedValues.Length > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) + if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) { yield return new ValidationError("Either allowed values or min and max value can be defined.", nameof(properties.AllowedValues), @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Length == 0)) + if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) { yield return new ValidationError("Radio buttons or dropdown list need allowed values.", nameof(properties.AllowedValues)); @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.MaxLength)); } - if (properties.AllowedValues != null && properties.AllowedValues.Length > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) + if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) { yield return new ValidationError("Either allowed values or min and max length can be defined.", nameof(properties.AllowedValues), diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs index 6502794b5..b9006a315 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Write.Schemas.Commands; using Squidex.Infrastructure; @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards { public static class GuardSchema { - public static Task CanCreate(CreateSchema command, ISchemaProvider schemas) + public static Task CanCreate(CreateSchema command, IAppProvider appProvider) { Guard.NotNull(command, nameof(command)); @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); } - if (await schemas.FindSchemaByNameAsync(command.AppId.Id, command.Name) != null) + if (await appProvider.GetSchemaAsync(command.AppId.Name, command.Name) != null) { error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); } diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs index 397622cb4..a651a264f 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs @@ -9,7 +9,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Write.Schemas.Commands; using Squidex.Domain.Apps.Write.Schemas.Guards; using Squidex.Infrastructure; @@ -20,23 +20,24 @@ namespace Squidex.Domain.Apps.Write.Schemas { public class SchemaCommandMiddleware : ICommandMiddleware { - private readonly ISchemaProvider schemas; + private readonly IAppProvider appProvider; private readonly IAggregateHandler handler; - public SchemaCommandMiddleware(IAggregateHandler handler, ISchemaProvider schemas) + public SchemaCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) { Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(schemas, nameof(schemas)); + Guard.NotNull(appProvider, nameof(appProvider)); this.handler = handler; - this.schemas = schemas; + + this.appProvider = appProvider; } protected Task On(CreateSchema command, CommandContext context) { return handler.CreateAsync(context, async s => { - await GuardSchema.CanCreate(command, schemas); + await GuardSchema.CanCreate(command, appProvider); s.Create(command); diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs index 877261b8a..bd7840709 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs @@ -56,62 +56,62 @@ namespace Squidex.Domain.Apps.Write.Schemas { totalFields++; - schema.Apply(@event, registry); + schema = schema.Apply(@event, registry); } protected void On(FieldUpdated @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldLocked @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldHidden @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldShown @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldDisabled @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldEnabled @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(SchemaUpdated @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(FieldDeleted @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(SchemaFieldsReordered @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(SchemaPublished @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(SchemaUnpublished @event) { - schema.Apply(@event); + schema = schema.Apply(@event); } protected void On(SchemaDeleted @event) diff --git a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj b/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj index 0efdb97e0..54328c3a8 100644 --- a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj +++ b/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/Squidex.Domain.Users.MongoDb/MongoXmlDocument.cs b/src/Squidex.Domain.Users.MongoDb/MongoXmlDocument.cs new file mode 100644 index 000000000..05181bde7 --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoXmlDocument.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// MongoXmlDocument.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoXmlDocument + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonRequired] + [BsonElement] + public string Xml { get; set; } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs b/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs new file mode 100644 index 000000000..ad7b7c953 --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// MongoXmlRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.Repositories; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoXmlRepository : MongoRepositoryBase, IXmlRepository + { + private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + + public MongoXmlRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Identity_XmlRepository"; + } + + public IReadOnlyCollection GetAllElements() + { + var elements = Collection.Find(new BsonDocument()).ToList(); + + return elements.Select(x => XElement.Parse(x.Xml)).ToList(); + } + + public void StoreElement(XElement element, string friendlyName) + { + Collection.UpdateOne(Filter.Eq(x => x.Id, friendlyName), + Update.Set(x => x.Xml, element.ToString()), + Upsert); + } + } +} 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 3e95a2ab6..d9b6e64e1 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -13,11 +13,11 @@ - - + + - + diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 11b5070ec..987174138 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index ad5236d3e..fdf7ccc90 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -72,6 +72,14 @@ namespace Squidex.Domain.Users return user; } + public static Task UpdateAsync(this UserManager userManager, IUser user, string email, string displayName) + { + user.UpdateEmail(email); + user.SetDisplayName(displayName); + + return userManager.UpdateAsync(user); + } + public static async Task UpdateAsync(this UserManager userManager, string id, string email, string displayName, string password) { var user = await userManager.FindByIdAsync(id); diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj index 5c79db93b..0c8a98854 100644 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -4,9 +4,9 @@ Squidex.Infrastructure - + - + diff --git a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index c06c834c2..c10907d3a 100644 --- a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj b/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj index 51ff2ad54..cce8551a3 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj +++ b/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs deleted file mode 100644 index 1d7f9acef..000000000 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// MongoEventConsumerInfoRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class MongoEventConsumerInfoRepository : MongoRepositoryBase, IEventConsumerInfoRepository - { - private static readonly FieldDefinition NameField = Fields.Build(x => x.Name); - private static readonly FieldDefinition ErrorField = Fields.Build(x => x.Error); - private static readonly FieldDefinition PositionField = Fields.Build(x => x.Position); - private static readonly FieldDefinition IsStoppedField = Fields.Build(x => x.IsStopped); - - public MongoEventConsumerInfoRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "EventPositions"; - } - - public async Task> QueryAsync() - { - var entities = await Collection.Find(new BsonDocument()).SortBy(x => x.Name).ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task FindAsync(string consumerName) - { - var entity = await Collection.Find(Filter.Eq(NameField, consumerName)).FirstOrDefaultAsync(); - - return entity; - } - - public Task ClearAsync(IEnumerable currentConsumerNames) - { - return Collection.DeleteManyAsync(Filter.Not(Filter.In(NameField, currentConsumerNames))); - } - - public async Task SetAsync(string consumerName, string position, bool isStopped = false, string error = null) - { - try - { - await Collection.UpdateOneAsync(Filter.Eq(NameField, consumerName), - Update - .Set(ErrorField, error) - .Set(PositionField, position) - .Set(IsStoppedField, isStopped), - new UpdateOptions { IsUpsert = true }); - } - catch (MongoWriteException ex) - { - if (ex.WriteError?.Category != ServerErrorCategory.DuplicateKey) - { - throw; - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs new file mode 100644 index 000000000..66e048d63 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// BsonHelper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.MongoDb +{ + public static class BsonHelper + { + public static string UnescapeBson(this string value) + { + return ReplaceFirstCharacter(value, '§', '$'); + } + + public static string EscapeJson(this string value) + { + return ReplaceFirstCharacter(value, '$', '§'); + } + + private static string ReplaceFirstCharacter(string value, char toReplace, char replacement) + { + if (value.Length == 0 || value[0] != toReplace) + { + return value; + } + + if (value.Length == 1) + { + return toReplace.ToString(); + } + + return replacement + value.Substring(1); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 62f163f47..b9964135e 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.MongoDb if (attributes.OfType().Any()) { - var bsonSerializerType = typeof(JsonBsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); memberMap.SetSerializer((IBsonSerializer)bsonSerializer); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs similarity index 93% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs index 100318609..20ec6e1ee 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs @@ -1,5 +1,5 @@ // ========================================================================== -// JsonBsonConverter.cs +// BsonJsonConverter.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,7 +12,7 @@ using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.MongoDb { - public static class JsonBsonConverter + public static class BsonJsonConverter { public static BsonDocument ToBson(this JObject source) { @@ -20,9 +20,7 @@ namespace Squidex.Infrastructure.MongoDb foreach (var property in source) { - var key = property.Key.Replace("$", "§"); - - result.Add(key, property.Value.ToBson()); + result.Add(property.Key.EscapeJson(), property.Value.ToBson()); } return result; @@ -34,9 +32,7 @@ namespace Squidex.Infrastructure.MongoDb foreach (var property in source) { - var key = property.Name.Replace("§", "$"); - - result.Add(key, property.Value.ToJson()); + result.Add(property.Name.UnescapeBson(), property.Value.ToJson()); } return result; diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs new file mode 100644 index 000000000..bd7003da0 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonReader.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// BsonJsonReader.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using NewtonsoftJsonReader = Newtonsoft.Json.JsonReader; +using NewtonsoftJsonToken = Newtonsoft.Json.JsonToken; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonReader : NewtonsoftJsonReader + { + private readonly IBsonReader bsonReader; + + public BsonJsonReader(IBsonReader bsonReader) + { + Guard.NotNull(bsonReader, nameof(bsonReader)); + + this.bsonReader = bsonReader; + } + + public override bool Read() + { + if (bsonReader.State == BsonReaderState.Initial || + bsonReader.State == BsonReaderState.ScopeDocument || + bsonReader.State == BsonReaderState.Type) + { + bsonReader.ReadBsonType(); + } + + if (bsonReader.State == BsonReaderState.Name) + { + SetToken(NewtonsoftJsonToken.PropertyName, bsonReader.ReadName().UnescapeBson()); + } + else if (bsonReader.State == BsonReaderState.EndOfDocument) + { + SetToken(NewtonsoftJsonToken.EndObject); + bsonReader.ReadEndDocument(); + } + else if (bsonReader.State == BsonReaderState.EndOfArray) + { + SetToken(NewtonsoftJsonToken.EndArray); + bsonReader.ReadEndArray(); + } + else if (bsonReader.State == BsonReaderState.Value) + { + switch (bsonReader.CurrentBsonType) + { + case BsonType.Document: + SetToken(NewtonsoftJsonToken.StartObject); + bsonReader.ReadStartDocument(); + break; + case BsonType.Array: + SetToken(NewtonsoftJsonToken.StartArray); + bsonReader.ReadStartArray(); + break; + case BsonType.Undefined: + SetToken(NewtonsoftJsonToken.Undefined); + bsonReader.ReadUndefined(); + break; + case BsonType.Null: + SetToken(NewtonsoftJsonToken.Null); + bsonReader.ReadNull(); + break; + case BsonType.String: + SetToken(NewtonsoftJsonToken.String, bsonReader.ReadString()); + break; + case BsonType.Binary: + SetToken(NewtonsoftJsonToken.Bytes, bsonReader.ReadBinaryData().Bytes); + break; + case BsonType.Boolean: + SetToken(NewtonsoftJsonToken.Boolean, bsonReader.ReadBoolean()); + break; + case BsonType.DateTime: + SetToken(NewtonsoftJsonToken.Date, bsonReader.ReadDateTime()); + break; + case BsonType.Int32: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt32()); + break; + case BsonType.Int64: + SetToken(NewtonsoftJsonToken.Integer, bsonReader.ReadInt64()); + break; + case BsonType.Double: + SetToken(NewtonsoftJsonToken.Float, bsonReader.ReadDouble()); + break; + case BsonType.Decimal128: + SetToken(NewtonsoftJsonToken.Float, Decimal128.ToDouble(bsonReader.ReadDecimal128())); + break; + default: + throw new NotSupportedException(); + } + } + + if (bsonReader.State == BsonReaderState.Initial) + { + return true; + } + + return !bsonReader.IsAtEndOfFile(); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs similarity index 73% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs index 996317c7b..f19987464 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -14,11 +14,11 @@ using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.MongoDb { - public class JsonBsonSerializer : ClassSerializerBase where T : class + public class BsonJsonSerializer : ClassSerializerBase where T : class { private readonly JsonSerializer serializer; - public JsonBsonSerializer(JsonSerializer serializer) + public BsonJsonSerializer(JsonSerializer serializer) { Guard.NotNull(serializer, nameof(serializer)); @@ -27,12 +27,16 @@ namespace Squidex.Infrastructure.MongoDb protected override T DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) { - return BsonSerializer.Deserialize(context.Reader).ToJson().ToObject(serializer); + var jsonReader = new BsonJsonReader(context.Reader); + + return serializer.Deserialize(jsonReader); } protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, T value) { - BsonSerializer.Serialize(context.Writer, JObject.FromObject(value, serializer).ToBson()); + var jsonWriter = new BsonJsonWriter(context.Writer); + + serializer.Serialize(jsonWriter, value); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs new file mode 100644 index 000000000..b4e923381 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs @@ -0,0 +1,166 @@ +// ========================================================================== +// BsonJsonWriter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson.IO; +using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class BsonJsonWriter : NewtonsoftJSonWriter + { + private readonly IBsonWriter bsonWriter; + + public BsonJsonWriter(IBsonWriter bsonWriter) + { + Guard.NotNull(bsonWriter, nameof(bsonWriter)); + + this.bsonWriter = bsonWriter; + } + + public override void WritePropertyName(string name, bool escape) + { + bsonWriter.WriteName(name.EscapeJson()); + } + + public override void WriteStartArray() + { + bsonWriter.WriteStartArray(); + } + + public override void WriteEndArray() + { + bsonWriter.WriteEndArray(); + } + + public override void WriteStartObject() + { + bsonWriter.WriteStartDocument(); + } + + public override void WriteEndObject() + { + bsonWriter.WriteEndDocument(); + } + + public override void WriteNull() + { + bsonWriter.WriteNull(); + } + + public override void WriteUndefined() + { + bsonWriter.WriteUndefined(); + } + + public override void WriteValue(string value) + { + bsonWriter.WriteString(value); + } + + public override void WriteValue(int value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(uint value) + { + bsonWriter.WriteInt32((int)value); + } + + public override void WriteValue(long value) + { + bsonWriter.WriteInt64(value); + } + + public override void WriteValue(ulong value) + { + bsonWriter.WriteInt64((long)value); + } + + public override void WriteValue(float value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(double value) + { + bsonWriter.WriteDouble(value); + } + + public override void WriteValue(bool value) + { + bsonWriter.WriteBoolean(value); + } + + public override void WriteValue(short value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(ushort value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(char value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(byte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(sbyte value) + { + bsonWriter.WriteInt32(value); + } + + public override void WriteValue(decimal value) + { + bsonWriter.WriteDecimal128(value); + } + + public override void WriteValue(DateTime value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(DateTimeOffset value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(byte[] value) + { + bsonWriter.WriteBytes(value); + } + + public override void WriteValue(TimeSpan value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Guid value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void WriteValue(Uri value) + { + bsonWriter.WriteString(value.ToString()); + } + + public override void Flush() + { + bsonWriter.Flush(); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs index 3ea809424..a480b310b 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoEntity.cs @@ -13,7 +13,7 @@ using NodaTime; namespace Squidex.Infrastructure.MongoDb { - public abstract class MongoEntity : IMongoEntity + public abstract class MongoEntity { [BsonId] [BsonElement] diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index babdf9e18..d636bd22e 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs similarity index 57% rename from src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs rename to src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index 07abec5cc..12ebab2e3 100644 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MongoEventConsumerInfo.cs +// MongoState.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,26 +8,24 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using Squidex.Infrastructure.MongoDb; -namespace Squidex.Infrastructure.CQRS.Events +namespace Squidex.Infrastructure.States { - [BsonIgnoreExtraElements] - public sealed class MongoEventConsumerInfo : IEventConsumerInfo + public sealed class MongoState { [BsonId] - [BsonRepresentation(BsonType.String)] - public string Name { get; set; } - [BsonElement] - [BsonIgnoreIfNull] - public string Error { get; set; } + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + [BsonRequired] [BsonElement] - [BsonIgnoreIfDefault] - public bool IsStopped { get; set; } + public string Etag { get; set; } - [BsonElement] [BsonRequired] - public string Position { get; set; } + [BsonElement] + [BsonJson] + public T Doc { get; set; } } -} \ No newline at end of file +} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoStateStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoStateStore.cs new file mode 100644 index 000000000..24b8e3f9b --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoStateStore.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// MongoStateStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.States +{ + public sealed class MongoStateStore : IStateStore, IExternalSystem + { + private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + private readonly IMongoDatabase database; + private readonly JsonSerializer serializer; + + public MongoStateStore(IMongoDatabase database, JsonSerializer serializer) + { + Guard.NotNull(database, nameof(database)); + Guard.NotNull(serializer, nameof(serializer)); + + this.database = database; + this.serializer = serializer; + } + + public void Connect() + { + try + { + database.ListCollections(); + } + catch (Exception ex) + { + throw new ConfigurationException($"MongoDb connection failed to connect to database {database.DatabaseNamespace.DatabaseName}", ex); + } + } + + public async Task<(T Value, string Etag)> ReadAsync(string key) + { + var collection = GetCollection(); + + var existing = + await collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.Doc, existing.Etag); + } + + return (default(T), null); + } + + public async Task WriteAsync(string key, T value, string oldEtag, string newEtag) + { + var collection = GetCollection(); + + try + { + await collection.UpdateOneAsync( + Builders>.Filter.And( + Builders>.Filter.Eq(x => x.Id, key), + Builders>.Filter.Eq(x => x.Etag, oldEtag) + ), + Builders>.Update + .Set(x => x.Doc, value) + .Set(x => x.Etag, newEtag), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingEtag = + await collection.Find(x => x.Id == key) + .Project>(Builders>.Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingEtag != null) + { + throw new InconsistentStateException(existingEtag.Etag, oldEtag, ex); + } + } + else + { + throw; + } + } + } + + private IMongoCollection> GetCollection() + { + return database.GetCollection>($"States_{typeof(T).Name}"); + } + } +} diff --git a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj index 9a9ae3306..dd33ddd4f 100644 --- a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs index 83a9929de..238b912d8 100644 --- a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs +++ b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs @@ -13,9 +13,9 @@ using StackExchange.Redis; namespace Squidex.Infrastructure { - public class RedisPubSub : IPubSub, IExternalSystem + public sealed class RedisPubSub : IPubSub, IExternalSystem { - private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary(); + private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary(); private readonly Lazy redisClient; private readonly Lazy redisSubscriber; private readonly ISemanticLog log; @@ -43,18 +43,21 @@ namespace Squidex.Infrastructure } } - public void Publish(string channelName, string token, bool notifySelf) + public void Publish(T value, bool notifySelf) { - Guard.NotNullOrEmpty(channelName, nameof(channelName)); + GetSubscriber().Publish(value, notifySelf); + } - subscriptions.GetOrAdd(channelName, c => new RedisSubscription(redisSubscriber.Value, c, log)).Publish(token, notifySelf); + public IDisposable Subscribe(Action handler) + { + return GetSubscriber().Subscribe(handler); } - public IDisposable Subscribe(string channelName, Action handler) + private RedisSubscription GetSubscriber() { - Guard.NotNullOrEmpty(channelName, nameof(channelName)); + var typeName = typeof(T).FullName; - return subscriptions.GetOrAdd(channelName, c => new RedisSubscription(redisSubscriber.Value, c, log)).Subscribe(handler); + return (RedisSubscription)subscriptions.GetOrAdd(typeName, c => new RedisSubscription(redisSubscriber.Value, c, log)); } } } diff --git a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs b/src/Squidex.Infrastructure.Redis/RedisSubscription.cs index 6ec078f70..9fbbacad1 100644 --- a/src/Squidex.Infrastructure.Redis/RedisSubscription.cs +++ b/src/Squidex.Infrastructure.Redis/RedisSubscription.cs @@ -7,49 +7,58 @@ // ========================================================================== using System; -using System.Linq; using System.Reactive.Subjects; +using Newtonsoft.Json; using Squidex.Infrastructure.Log; using StackExchange.Redis; +#pragma warning disable SA1401 // Fields must be private + namespace Squidex.Infrastructure { - internal sealed class RedisSubscription + internal sealed class RedisSubscription { - private static readonly Guid InstanceId = Guid.NewGuid(); - private readonly Subject subject = new Subject(); + private readonly Guid selfId = Guid.NewGuid(); + private readonly Subject subject = new Subject(); private readonly ISubscriber subscriber; - private readonly string channelName; private readonly ISemanticLog log; + private readonly string channelName; + + private sealed class Envelope + { + public T Payload; + + public Guid Sender; + } public RedisSubscription(ISubscriber subscriber, string channelName, ISemanticLog log) { this.log = log; this.subscriber = subscriber; - this.subscriber.Subscribe(channelName, (channel, value) => HandleInvalidation(value)); + this.subscriber.Subscribe(channelName, (channel, value) => HandleMessage(value)); this.channelName = channelName; } - public void Publish(string token, bool notifySelf) + public void Publish(object value, bool notifySelf) { try { - var message = string.Join("#", (notifySelf ? Guid.Empty : InstanceId).ToString(), token); + var envelope = JsonConvert.SerializeObject(new Envelope { Sender = selfId, Payload = (T)value }); - subscriber.Publish(channelName, message); + subscriber.Publish(channelName, envelope); } catch (Exception ex) { log.LogError(ex, w => w .WriteProperty("action", "PublishRedisMessage") .WriteProperty("state", "Failed") - .WriteProperty("token", token)); + .WriteProperty("channel", channelName)); } } - private void HandleInvalidation(string value) + private void HandleMessage(string value) { try { @@ -58,28 +67,15 @@ namespace Squidex.Infrastructure return; } - var parts = value.Split('#'); - - if (parts.Length < 1) - { - return; - } - - if (!Guid.TryParse(parts[0], out var sender)) - { - return; - } + var envelope = JsonConvert.DeserializeObject(value); - if (sender != InstanceId) + if (envelope.Sender != selfId) { - var token = string.Join("#", parts.Skip(1)); - - subject.OnNext(token); + subject.OnNext(envelope.Payload); log.LogDebug(w => w .WriteProperty("action", "ReceiveRedisMessage") .WriteProperty("channel", channelName) - .WriteProperty("token", token) .WriteProperty("state", "Received")); } } @@ -92,7 +88,7 @@ namespace Squidex.Infrastructure } } - public IDisposable Subscribe(Action handler) + public IDisposable Subscribe(Action handler) { return subject.Subscribe(handler); } diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj index 5790bb386..0a815a472 100644 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs b/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs deleted file mode 100644 index b7a4b94fc..000000000 --- a/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// DefaultRemoteActorChannel.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Actors -{ - public sealed class DefaultRemoteActorChannel : IRemoteActorChannel - { - private static readonly string ChannelName = typeof(DefaultRemoteActorChannel).Name; - private readonly IPubSub pubSub; - private readonly JsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry; - - private sealed class Envelope - { - public string Recipient { get; set; } - - public string PayloadType { get; set; } - - public JToken Payload { get; set; } - } - - public DefaultRemoteActorChannel(IPubSub pubSub, TypeNameRegistry typeNameRegistry, JsonSerializerSettings serializerSettings = null) - { - Guard.NotNull(pubSub, nameof(pubSub)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); - - this.pubSub = pubSub; - - this.typeNameRegistry = typeNameRegistry; - - serializer = JsonSerializer.Create(serializerSettings ?? new JsonSerializerSettings()); - } - - public Task SendAsync(string recipient, object message) - { - Guard.NotNullOrEmpty(recipient, nameof(recipient)); - Guard.NotNull(message, nameof(message)); - - var messageType = typeNameRegistry.GetName(message.GetType()); - var messageBody = WriteJson(message); - - var envelope = new Envelope { Recipient = recipient, Payload = messageBody, PayloadType = messageType }; - - pubSub.Publish(ChannelName, JsonConvert.SerializeObject(envelope), true); - - return TaskHelper.Done; - } - - public void Subscribe(string recipient, Action handler) - { - Guard.NotNullOrEmpty(recipient, nameof(recipient)); - - pubSub.Subscribe(ChannelName, json => - { - var envelope = JsonConvert.DeserializeObject(json); - - if (string.Equals(envelope.Recipient, recipient, StringComparison.OrdinalIgnoreCase)) - { - var messageType = typeNameRegistry.GetType(envelope.PayloadType); - var messageBody = ReadJson(envelope.Payload, messageType); - - handler?.Invoke(messageBody); - } - }); - } - - private object ReadJson(JToken token, Type type) - { - return token.ToObject(type, serializer); - } - - private JToken WriteJson(object value) - { - return JToken.FromObject(value, serializer); - } - } -} diff --git a/src/Squidex.Infrastructure/Actors/RemoteActors.cs b/src/Squidex.Infrastructure/Actors/RemoteActors.cs deleted file mode 100644 index 268c765ce..000000000 --- a/src/Squidex.Infrastructure/Actors/RemoteActors.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// RemoteActors.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Concurrent; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Actors -{ - public sealed class RemoteActors : IActors - { - private readonly ConcurrentDictionary senders = new ConcurrentDictionary(); - private readonly ConcurrentDictionary receivers = new ConcurrentDictionary(); - private readonly IRemoteActorChannel channel; - - private sealed class Sender : IActor - { - private readonly IRemoteActorChannel channel; - private readonly string recipient; - - public Sender(IRemoteActorChannel channel, string recipient) - { - this.recipient = recipient; - - this.channel = channel; - } - - public void Tell(object message) - { - channel.SendAsync(recipient, message).Forget(); - } - } - - public RemoteActors(IRemoteActorChannel channel) - { - Guard.NotNull(channel, nameof(channel)); - - this.channel = channel; - } - - public IActor Get(string id) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - return senders.GetOrAdd(id, k => new Sender(channel, id)); - } - - public void Connect(string id, IActor actor) - { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNull(actor, nameof(actor)); - - channel.Subscribe(id, actor.Tell); - } - } -} diff --git a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs b/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs index 86a5a9f92..92e9009cc 100644 --- a/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs +++ b/src/Squidex.Infrastructure/Assets/AssetNotFoundException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure.Assets { + [Serializable] public class AssetNotFoundException : Exception { public AssetNotFoundException() @@ -25,5 +27,10 @@ namespace Squidex.Infrastructure.Assets : base(message, inner) { } + + protected AssetNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs index b89fb0957..c8f722b6f 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs @@ -8,47 +8,34 @@ using System; using System.Threading.Tasks; -using Squidex.Infrastructure.Actors; -using Squidex.Infrastructure.CQRS.Events.Actors.Messages; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events.Actors { - public class EventConsumerActor : DisposableObjectBase, IEventSubscriber, IActor + public class EventConsumerActor : StatefulObject, IEventSubscriber { private readonly EventDataFormatter formatter; private readonly IEventStore eventStore; - private readonly IEventConsumerInfoRepository eventConsumerInfoRepository; private readonly ISemanticLog log; private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1); private IEventSubscription currentSubscription; private IEventConsumer eventConsumer; - private bool statusIsRunning = true; - private string statusPosition; - private string statusError; - - private static Func DefaultFactory - { - get { return (e, s, t, p) => new RetrySubscription(e, s, t, p); } - } public EventConsumerActor( EventDataFormatter formatter, IEventStore eventStore, - IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log) { Guard.NotNull(log, nameof(log)); Guard.NotNull(formatter, nameof(formatter)); Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository)); this.log = log; this.formatter = formatter; this.eventStore = eventStore; - this.eventConsumerInfoRepository = eventConsumerInfoRepository; } protected override void DisposeObject(bool disposing) @@ -64,30 +51,43 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors return new RetrySubscription(eventStore, this, streamFilter, position); } - public Task SubscribeAsync(IEventConsumer eventConsumer) + public virtual EventConsumerInfo GetState() + { + return State.ToInfo(this.eventConsumer.Name); + } + + public virtual void Stop() + { + dispatcher.DispatchAsync(() => HandleStopAsync()).Forget(); + } + + public virtual void Start() + { + dispatcher.DispatchAsync(() => HandleStartAsync()).Forget(); + } + + public virtual void Reset() + { + dispatcher.DispatchAsync(() => HandleResetAsync()).Forget(); + } + + public virtual void Activate(IEventConsumer eventConsumer) { Guard.NotNull(eventConsumer, nameof(eventConsumer)); - return dispatcher.DispatchAsync(() => HandleSetupAsync(eventConsumer)); + dispatcher.DispatchAsync(() => HandleSetupAsync(eventConsumer)).Forget(); } - private async Task HandleSetupAsync(IEventConsumer consumer) + private Task HandleSetupAsync(IEventConsumer consumer) { eventConsumer = consumer; - var status = await eventConsumerInfoRepository.FindAsync(eventConsumer.Name); - - if (status != null) + if (!State.IsStopped) { - statusError = status.Error; - statusPosition = status.Position; - statusIsRunning = !status.IsStopped; + Subscribe(State.Position); } - if (statusIsRunning) - { - Subscribe(statusPosition); - } + return TaskHelper.Done; } private Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) @@ -106,8 +106,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors await DispatchConsumerAsync(@event); } - statusError = null; - statusPosition = storedEvent.EventPosition; + State = State.Handled(storedEvent.EventPosition); }); } @@ -122,30 +121,28 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors { Unsubscribe(); - statusError = exception.ToString(); - statusIsRunning = false; + State = State.Failed(exception); }); } private Task HandleStartAsync() { - if (statusIsRunning) + if (!State.IsStopped) { return TaskHelper.Done; } return DoAndUpdateStateAsync(() => { - Subscribe(statusPosition); + Subscribe(State.Position); - statusError = null; - statusIsRunning = true; + State = State.Started(); }); } private Task HandleStopAsync() { - if (!statusIsRunning) + if (State.IsStopped) { return TaskHelper.Done; } @@ -154,12 +151,11 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors { Unsubscribe(); - statusError = null; - statusIsRunning = false; + State = State.Stopped(); }); } - private Task HandleResetInternalAsync() + private Task HandleResetAsync() { return DoAndUpdateStateAsync(async () => { @@ -169,9 +165,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors Subscribe(null); - statusError = null; - statusPosition = null; - statusIsRunning = true; + State = State.Reset(); }); } @@ -185,24 +179,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); } - void IActor.Tell(object message) - { - switch (message) - { - case StopConsumerMessage stop: - dispatcher.DispatchAsync(() => HandleStopAsync()).Forget(); - break; - - case StartConsumerMessage stop: - dispatcher.DispatchAsync(() => HandleStartAsync()).Forget(); - break; - - case ResetConsumerMessage stop: - dispatcher.DispatchAsync(() => HandleResetInternalAsync()).Forget(); - break; - } - } - private Task DoAndUpdateStateAsync(Action action) { return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }); @@ -213,7 +189,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors try { await action(); - await eventConsumerInfoRepository.SetAsync(eventConsumer.Name, statusPosition, !statusIsRunning, statusError); } catch (Exception ex) { @@ -231,11 +206,10 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors .WriteProperty("state", "Failed") .WriteProperty("eventConsumer", eventConsumer.Name)); - statusError = ex.ToString(); - statusIsRunning = false; - - await eventConsumerInfoRepository.SetAsync(eventConsumer.Name, statusPosition, !statusIsRunning, statusError); + State = State.Failed(ex); } + + await WriteStateAsync(); } private async Task ClearAsync() diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActorManager.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActorManager.cs new file mode 100644 index 000000000..9045748f0 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActorManager.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// EventConsumerActorManager.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.CQRS.Events.Actors.Messages; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.CQRS.Events.Actors +{ + public sealed class EventConsumerActorManager : DisposableObjectBase, IExternalSystem + { + private readonly IStateFactory factory; + private readonly IPubSub pubSub; + private readonly List consumers; + private readonly List subscriptions = new List(); + + public EventConsumerActorManager(IEnumerable consumers, IPubSub pubSub, IStateFactory factory) + { + Guard.NotNull(pubSub, nameof(pubSub)); + Guard.NotNull(factory, nameof(factory)); + Guard.NotNull(consumers, nameof(consumers)); + + this.pubSub = pubSub; + this.factory = factory; + this.consumers = consumers.ToList(); + } + + public void Connect() + { + var actors = new Dictionary(); + + foreach (var consumer in consumers) + { + var actor = factory.GetAsync(consumer.Name).Result; + + actors[consumer.Name] = actor; + actor.Activate(consumer); + } + + subscriptions.Add(pubSub.Subscribe(m => + { + if (actors.TryGetValue(m.ConsumerName, out var actor)) + { + actor.Start(); + } + })); + + subscriptions.Add(pubSub.Subscribe(m => + { + if (actors.TryGetValue(m.ConsumerName, out var actor)) + { + actor.Stop(); + } + })); + + subscriptions.Add(pubSub.Subscribe(m => + { + if (actors.TryGetValue(m.ConsumerName, out var actor)) + { + actor.Reset(); + } + })); + + subscriptions.Add(pubSub.ReceiveAsync(request => + { + var states = actors.Values.Select(x => x.GetState()).ToArray(); + + return Task.FromResult(new GetStatesResponse { States = states }); + })); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + foreach (var subscription in subscriptions) + { + subscription.Dispose(); + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerState.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerState.cs new file mode 100644 index 000000000..9e456e10b --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerState.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// EventConsumerGrainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Infrastructure.CQRS.Events.Actors +{ + public sealed class EventConsumerState + { + public bool IsStopped { get; set; } + + public string Error { get; set; } + + public string Position { get; set; } + + public EventConsumerState Reset() + { + return new EventConsumerState(); + } + + public EventConsumerState Handled(string position) + { + return new EventConsumerState { Position = position }; + } + + public EventConsumerState Failed(Exception ex) + { + return new EventConsumerState { Position = Position, IsStopped = true, Error = ex?.ToString() }; + } + + public EventConsumerState Stopped() + { + return new EventConsumerState { Position = Position, IsStopped = true }; + } + + public EventConsumerState Started() + { + return new EventConsumerState { Position = Position, IsStopped = false }; + } + + public EventConsumerInfo ToInfo(string name) + { + return SimpleMapper.Map(this, new EventConsumerInfo { Name = name }); + } + } +} diff --git a/src/Squidex.Infrastructure/Actors/IActors.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesRequest.cs similarity index 72% rename from src/Squidex.Infrastructure/Actors/IActors.cs rename to src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesRequest.cs index b5f1c84bb..9e67dbcb7 100644 --- a/src/Squidex.Infrastructure/Actors/IActors.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesRequest.cs @@ -1,15 +1,14 @@ // ========================================================================== -// IActors.cs +// GetStatesRequest.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.Actors +namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages { - public interface IActors + public sealed class GetStatesRequest { - IActor Get(string id); } -} \ No newline at end of file +} diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesResponse.cs similarity index 64% rename from src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs rename to src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesResponse.cs index c34d229cf..95d23de57 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/GetStatesResponse.cs @@ -1,16 +1,15 @@ // ========================================================================== -// IAppEventConsumer.cs +// GetStatesResponse.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.CQRS.Events; - -namespace Squidex.Domain.Apps.Read.Apps +namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages { - public interface IAppEventConsumer : IEventConsumer + public sealed class GetStatesResponse { + public EventConsumerInfo[] States { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs index 562514bc8..f2e09aca2 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs @@ -8,8 +8,8 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages { - [TypeName(nameof(ResetConsumerMessage))] public sealed class ResetConsumerMessage { + public string ConsumerName { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs index 89850d713..29fa49e1b 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs @@ -8,8 +8,8 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages { - [TypeName(nameof(StartConsumerMessage))] public sealed class StartConsumerMessage { + public string ConsumerName { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs index 071269b8c..ea32b4b17 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs @@ -8,8 +8,8 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages { - [TypeName(nameof(StopConsumerMessage))] public sealed class StopConsumerMessage { + public string ConsumerName { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs index 69ab25e88..d716c4b52 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs @@ -16,6 +16,11 @@ namespace Squidex.Infrastructure.CQRS.Events private readonly IPubSub pubsub; + public sealed class EventNotification + { + public string StreamName { get; set; } + } + public DefaultEventNotifier(IPubSub pubsub) { Guard.NotNull(pubsub, nameof(pubsub)); @@ -25,12 +30,12 @@ namespace Squidex.Infrastructure.CQRS.Events public void NotifyEventsStored(string streamName) { - pubsub.Publish(ChannelName, streamName, true); + pubsub.Publish(new EventNotification { StreamName = streamName }, true); } public IDisposable Subscribe(Action handler) { - return pubsub.Subscribe(ChannelName, x => handler?.Invoke(x)); + return pubsub.Subscribe(x => handler?.Invoke(x.StreamName)); } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Envelope{T}.cs b/src/Squidex.Infrastructure/CQRS/Events/Envelope{T}.cs index c03435422..64e6fa29a 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Envelope{T}.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Envelope{T}.cs @@ -23,23 +23,12 @@ namespace Squidex.Infrastructure.CQRS.Events get { return payload; } } - public Envelope(T payload) - : this(payload, new EnvelopeHeaders()) - { - } - - public Envelope(T payload, PropertiesBag bag) - : this(payload, new EnvelopeHeaders(bag)) - { - } - - public Envelope(T payload, EnvelopeHeaders headers) + public Envelope(T payload, EnvelopeHeaders headers = null) { Guard.NotNull(payload, nameof(payload)); - Guard.NotNull(headers, nameof(headers)); this.payload = payload; - this.headers = headers; + this.headers = headers ?? new EnvelopeHeaders(); } public Envelope To() where TOther : class diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs b/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs deleted file mode 100644 index c51c7b043..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ========================================================================== -// EventConsumerCleaner.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class EventConsumerCleaner - { - private readonly IEnumerable eventConsumers; - private readonly IEventConsumerInfoRepository eventConsumerInfoRepository; - - public EventConsumerCleaner(IEnumerable eventConsumers, IEventConsumerInfoRepository eventConsumerInfoRepository) - { - Guard.NotNull(eventConsumers, nameof(eventConsumers)); - Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository)); - - this.eventConsumers = eventConsumers; - this.eventConsumerInfoRepository = eventConsumerInfoRepository; - } - - public Task CleanAsync() - { - var names = eventConsumers.Select(x => x.Name).ToArray(); - - return eventConsumerInfoRepository.ClearAsync(names); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs b/src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs new file mode 100644 index 000000000..08fbdf24c --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// EventConsumerInfo.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Events +{ + public sealed class EventConsumerInfo + { + public bool IsStopped { get; set; } + + public string Name { get; set; } + + public string Error { get; set; } + + public string Position { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs b/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs index 60d6f7095..939577d31 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.CQRS.Events public virtual Envelope Parse(EventData eventData, bool migrate = true) { - var headers = ReadJson(eventData.Metadata); + var headers = ReadJson(eventData.Metadata); var eventType = typeNameRegistry.GetType(eventData.Type); var eventPayload = ReadJson(eventData.Payload, eventType); diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs index 1cb4fba4d..565b35d50 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs @@ -10,6 +10,8 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.CQRS.Events { + public delegate IEventConsumer EventConsumerFactory(string name); + public interface IEventConsumer { string Name { get; } diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs deleted file mode 100644 index f074ee5ef..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// IEventConsumerInfo.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -namespace Squidex.Infrastructure.CQRS.Events -{ - public interface IEventConsumerInfo - { - bool IsStopped { get; } - - string Name { get; } - - string Error { get; } - - string Position { get; } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs deleted file mode 100644 index f44d4de57..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// IEventConsumerInfoRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public interface IEventConsumerInfoRepository - { - Task> QueryAsync(); - - Task FindAsync(string consumerName); - - Task ClearAsync(IEnumerable currentConsumerNames); - - Task SetAsync(string consumerName, string position, bool isStopped, string error = null); - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs b/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs index 5c232455b..e14ea7e43 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs @@ -9,7 +9,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Squidex.Infrastructure.Actors; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events @@ -17,7 +16,7 @@ namespace Squidex.Infrastructure.CQRS.Events public sealed class RetrySubscription : IEventSubscription, IEventSubscriber { private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); - private readonly CancellationTokenSource disposeCts = new CancellationTokenSource(); + private readonly CancellationTokenSource timerCts = new CancellationTokenSource(); private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); private readonly IEventStore eventStore; private readonly IEventSubscriber eventSubscriber; @@ -44,12 +43,16 @@ namespace Squidex.Infrastructure.CQRS.Events private void Subscribe() { - currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); + if (currentSubscription == null) + { + currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); + } } private void Unsubscribe() { currentSubscription?.StopAsync().Forget(); + currentSubscription = null; } private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) @@ -66,12 +69,11 @@ namespace Squidex.Infrastructure.CQRS.Events { if (subscription == currentSubscription) { - subscription.StopAsync().Forget(); - subscription = null; + Unsubscribe(); if (retryWindow.CanRetryAfterFailure()) { - Task.Delay(ReconnectWaitMs, disposeCts.Token).ContinueWith(t => + Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t => { dispatcher.DispatchAsync(() => Subscribe()); }).Forget(); @@ -98,7 +100,7 @@ namespace Squidex.Infrastructure.CQRS.Events await dispatcher.DispatchAsync(() => Unsubscribe()); await dispatcher.StopAndWaitAsync(); - disposeCts.Cancel(); + timerCts.Cancel(); } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs b/src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs index 030197c3f..afa2c92ff 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/WrongEventVersionException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure.CQRS.Events { + [Serializable] public class WrongEventVersionException : Exception { private readonly long currentVersion; @@ -33,6 +35,11 @@ namespace Squidex.Infrastructure.CQRS.Events this.expectedVersion = expectedVersion; } + protected WrongEventVersionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + private static string FormatMessage(long currentVersion, long expectedVersion) { return $"Requested version {expectedVersion}, but found {currentVersion}."; diff --git a/src/Squidex.Infrastructure/Caching/InvalidateMessage.cs b/src/Squidex.Infrastructure/Caching/InvalidateMessage.cs new file mode 100644 index 000000000..db8c81c32 --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/InvalidateMessage.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// InvalidateMessage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.Caching +{ + public sealed class InvalidateMessage + { + public string CacheKey { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/Caching/InvalidatingMemoryCache.cs b/src/Squidex.Infrastructure/Caching/InvalidatingMemoryCache.cs index 4ce50f1e7..2e9261ed9 100644 --- a/src/Squidex.Infrastructure/Caching/InvalidatingMemoryCache.cs +++ b/src/Squidex.Infrastructure/Caching/InvalidatingMemoryCache.cs @@ -6,14 +6,15 @@ // All rights reserved. // ========================================================================== +using System; using Microsoft.Extensions.Caching.Memory; namespace Squidex.Infrastructure.Caching { - public class InvalidatingMemoryCache : IMemoryCache, IInvalidatingCache + public class InvalidatingMemoryCache : DisposableObjectBase, IMemoryCache, IInvalidatingCache { - private const string ChannelName = "CacheInvalidations"; private readonly IMemoryCache inner; + private readonly IDisposable subscription; private readonly IPubSub invalidator; public InvalidatingMemoryCache(IMemoryCache inner, IPubSub invalidator) @@ -24,12 +25,20 @@ namespace Squidex.Infrastructure.Caching this.inner = inner; this.invalidator = invalidator; - invalidator.Subscribe(ChannelName, inner.Remove); + subscription = invalidator.Subscribe(m => + { + inner.Remove(m.CacheKey); + }); } - public void Dispose() + protected override void DisposeObject(bool disposing) { - inner.Dispose(); + if (disposing) + { + subscription.Dispose(); + + inner.Dispose(); + } } public ICacheEntry CreateEntry(object key) @@ -49,9 +58,9 @@ namespace Squidex.Infrastructure.Caching public void Invalidate(object key) { - if (key is string) + if (key is string stringKey) { - invalidator.Publish(ChannelName, key.ToString(), true); + invalidator.Publish(new InvalidateMessage { CacheKey = stringKey }, true); } } } diff --git a/src/Squidex.Infrastructure/Caching/LRUCache.cs b/src/Squidex.Infrastructure/Caching/LRUCache.cs new file mode 100644 index 000000000..95065c214 --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/LRUCache.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// LRUCache.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class LRUCache + { + private readonly Dictionary> cacheMap = new Dictionary>(); + private readonly LinkedList cacheHistory = new LinkedList(); + private readonly int capacity; + + public LRUCache(int capacity) + { + Guard.GreaterThan(capacity, 0, nameof(capacity)); + + this.capacity = capacity; + } + + public bool Set(object key, object value) + { + Guard.NotNull(key, nameof(key)); + + if (cacheMap.TryGetValue(key, out var node)) + { + node.Value.Value = value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + cacheMap[key] = node; + + return true; + } + else + { + if (cacheMap.Count >= capacity) + { + RemoveFirst(); + } + + var cacheItem = new LRUCacheItem { Key = key, Value = value }; + + node = new LinkedListNode(cacheItem); + + cacheMap.Add(key, node); + cacheHistory.AddLast(node); + + return false; + } + } + + public bool Remove(object key) + { + Guard.NotNull(key, nameof(key)); + + if (cacheMap.TryGetValue(key, out var node)) + { + cacheMap.Remove(key); + cacheHistory.Remove(node); + + return true; + } + + return false; + } + + public bool TryGetValue(object key, out object value) + { + Guard.NotNull(key, nameof(key)); + + value = null; + + if (cacheMap.TryGetValue(key, out var node)) + { + value = node.Value.Value; + + cacheHistory.Remove(node); + cacheHistory.AddLast(node); + + return true; + } + + return false; + } + + public bool Contains(object key) + { + Guard.NotNull(key, nameof(key)); + + return cacheMap.ContainsKey(key); + } + + private void RemoveFirst() + { + var node = cacheHistory.First; + + cacheMap.Remove(node.Value.Key); + cacheHistory.RemoveFirst(); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs new file mode 100644 index 000000000..942fdd15d --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// LRUCacheItem.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Infrastructure.Caching +{ + internal class LRUCacheItem + { + public object Key; + public object Value; + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Cloneable.cs b/src/Squidex.Infrastructure/Cloneable.cs new file mode 100644 index 000000000..eabcc4e27 --- /dev/null +++ b/src/Squidex.Infrastructure/Cloneable.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Cloneable.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public abstract class Cloneable + { + protected T Clone(Action updater) where T : Cloneable + { + var clone = (T)MemberwiseClone(); + + updater(clone); + + clone.OnCloned(); + + return clone; + } + + protected virtual void OnCloned() + { + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/IMongoEntity.cs b/src/Squidex.Infrastructure/Cloneable{T}.cs similarity index 60% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/IMongoEntity.cs rename to src/Squidex.Infrastructure/Cloneable{T}.cs index 5dee147a4..56877f04d 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/IMongoEntity.cs +++ b/src/Squidex.Infrastructure/Cloneable{T}.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IMongoEntity.cs +// Cloneable{T}.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,16 +7,14 @@ // ========================================================================== using System; -using NodaTime; -namespace Squidex.Infrastructure.MongoDb +namespace Squidex.Infrastructure { - public interface IMongoEntity + public abstract class Cloneable : Cloneable where T : Cloneable { - Guid Id { get; set; } - - Instant Created { get; set; } - - Instant LastModified { get; set; } + protected T Clone(Action updater) + { + return base.Clone(updater); + } } } diff --git a/src/Squidex.Infrastructure/ConfigurationException.cs b/src/Squidex.Infrastructure/ConfigurationException.cs index ce116635d..158447040 100644 --- a/src/Squidex.Infrastructure/ConfigurationException.cs +++ b/src/Squidex.Infrastructure/ConfigurationException.cs @@ -7,10 +7,12 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { - public sealed class ConfigurationException : Exception + [Serializable] + public class ConfigurationException : Exception { public ConfigurationException() { @@ -25,5 +27,10 @@ namespace Squidex.Infrastructure : base(message, inner) { } + + protected ConfigurationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/DomainException.cs b/src/Squidex.Infrastructure/DomainException.cs index 6fa8f9be7..89e3767fa 100644 --- a/src/Squidex.Infrastructure/DomainException.cs +++ b/src/Squidex.Infrastructure/DomainException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainException : Exception { public DomainException(string message) @@ -21,5 +23,10 @@ namespace Squidex.Infrastructure : base(message, inner) { } + + protected DomainException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/DomainForbiddenException.cs b/src/Squidex.Infrastructure/DomainForbiddenException.cs index 13a838583..49a45957e 100644 --- a/src/Squidex.Infrastructure/DomainForbiddenException.cs +++ b/src/Squidex.Infrastructure/DomainForbiddenException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainForbiddenException : DomainException { public DomainForbiddenException(string message) @@ -21,5 +23,10 @@ namespace Squidex.Infrastructure : base(message, inner) { } + + protected DomainForbiddenException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/DomainObjectDeletedException.cs b/src/Squidex.Infrastructure/DomainObjectDeletedException.cs index 17d27bdf0..307d52d2b 100644 --- a/src/Squidex.Infrastructure/DomainObjectDeletedException.cs +++ b/src/Squidex.Infrastructure/DomainObjectDeletedException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainObjectDeletedException : DomainObjectException { public DomainObjectDeletedException(string id, Type type) @@ -17,6 +19,11 @@ namespace Squidex.Infrastructure { } + protected DomainObjectDeletedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + private static string FormatMessage(string id, Type type) { return $"Domain object \'{id}\' (type {type}) already deleted."; diff --git a/src/Squidex.Infrastructure/DomainObjectException.cs b/src/Squidex.Infrastructure/DomainObjectException.cs index bfa481488..cf5498093 100644 --- a/src/Squidex.Infrastructure/DomainObjectException.cs +++ b/src/Squidex.Infrastructure/DomainObjectException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainObjectException : Exception { private readonly string id; @@ -32,5 +34,10 @@ namespace Squidex.Infrastructure typeName = type?.Name; } + + protected DomainObjectException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs b/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs index 65d3872d2..472259bf0 100644 --- a/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs +++ b/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainObjectNotFoundException : DomainObjectException { public DomainObjectNotFoundException(string id, Type type) @@ -22,6 +24,11 @@ namespace Squidex.Infrastructure { } + protected DomainObjectNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + private static string FormatMessage(string id, Type type) { return $"Domain object \'{id}\' (type {type}) is not found."; diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 19599594d..2d0af3ba6 100644 --- a/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class DomainObjectVersionException : DomainObjectException { private readonly long currentVersion; @@ -33,6 +35,11 @@ namespace Squidex.Infrastructure this.expectedVersion = expectedVersion; } + protected DomainObjectVersionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + private static string FormatMessage(string id, Type type, long currentVersion, long expectedVersion) { return $"Requested version {expectedVersion} for object '{id}' (type {type}), but found {currentVersion}."; diff --git a/src/Squidex.Infrastructure/Freezable.cs b/src/Squidex.Infrastructure/Freezable.cs new file mode 100644 index 000000000..378cc3f94 --- /dev/null +++ b/src/Squidex.Infrastructure/Freezable.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Freezable.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public abstract class Freezable + { + public bool IsFrozen { get; private set; } + + protected void ThrowIfFrozen() + { + if (IsFrozen) + { + throw new InvalidOperationException("Object is frozen"); + } + } + + public void Freeze() + { + IsFrozen = true; + } + } +} diff --git a/src/Squidex.Infrastructure/IPubSub.cs b/src/Squidex.Infrastructure/IPubSub.cs index cee212fac..b06586aa6 100644 --- a/src/Squidex.Infrastructure/IPubSub.cs +++ b/src/Squidex.Infrastructure/IPubSub.cs @@ -12,8 +12,8 @@ namespace Squidex.Infrastructure { public interface IPubSub { - void Publish(string channelName, string token, bool notifySelf); + void Publish(T value, bool notifySelf); - IDisposable Subscribe(string channelName, Action handler); + IDisposable Subscribe(Action handler); } } diff --git a/src/Squidex.Infrastructure/InMemoryPubSub.cs b/src/Squidex.Infrastructure/InMemoryPubSub.cs index 3d0307371..a4ace58fd 100644 --- a/src/Squidex.Infrastructure/InMemoryPubSub.cs +++ b/src/Squidex.Infrastructure/InMemoryPubSub.cs @@ -7,26 +7,26 @@ // ========================================================================== using System; -using System.Collections.Concurrent; +using System.Reactive.Linq; using System.Reactive.Subjects; namespace Squidex.Infrastructure { public sealed class InMemoryPubSub : IPubSub { - private readonly ConcurrentDictionary> subjects = new ConcurrentDictionary>(); + private readonly Subject subject = new Subject(); - public void Publish(string channelName, string token, bool notifySelf) + public void Publish(T value, bool notifySelf) { if (notifySelf) { - subjects.GetOrAdd(channelName, k => new Subject()).OnNext(token); + subject.OnNext(value); } } - public IDisposable Subscribe(string channelName, Action handler) + public IDisposable Subscribe(Action handler) { - return subjects.GetOrAdd(channelName, k => new Subject()).Subscribe(handler); + return subject.Where(x => x is T).OfType().Subscribe(handler); } } } diff --git a/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs b/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs index aca3cdf7f..220086706 100644 --- a/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs +++ b/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Linq; using System.Security.Claims; using Newtonsoft.Json; @@ -48,7 +49,7 @@ namespace Squidex.Infrastructure.Json serializer.Serialize(writer, jsonIdentities); } - protected override ClaimsPrincipal ReadValue(JsonReader reader, JsonSerializer serializer) + protected override ClaimsPrincipal ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { var jsonIdentities = serializer.Deserialize(reader); diff --git a/src/Squidex.Infrastructure/Json/JsonClassConverter.cs b/src/Squidex.Infrastructure/Json/JsonClassConverter.cs index a784d6397..5053625be 100644 --- a/src/Squidex.Infrastructure/Json/JsonClassConverter.cs +++ b/src/Squidex.Infrastructure/Json/JsonClassConverter.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.Json return null; } - return ReadValue(reader, serializer); + return ReadValue(reader, objectType, serializer); } public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -41,6 +41,6 @@ namespace Squidex.Infrastructure.Json protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); - protected abstract T ReadValue(JsonReader reader, JsonSerializer serializer); + protected abstract T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer); } } diff --git a/src/Squidex.Infrastructure/Json/LanguageConverter.cs b/src/Squidex.Infrastructure/Json/LanguageConverter.cs index 25d2fa296..73e9a8c85 100644 --- a/src/Squidex.Infrastructure/Json/LanguageConverter.cs +++ b/src/Squidex.Infrastructure/Json/LanguageConverter.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json @@ -17,7 +18,7 @@ namespace Squidex.Infrastructure.Json writer.WriteValue(value.Iso2Code); } - protected override Language ReadValue(JsonReader reader, JsonSerializer serializer) + protected override Language ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.String) { diff --git a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs index 9e572d2c3..968cb3762 100644 --- a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure.Json writer.WriteValue($"{value.Id},{value.Name}"); } - protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.String) { diff --git a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs index f81b62650..4829f79c4 100644 --- a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure.Json writer.WriteValue($"{value.Id},{value.Name}"); } - protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.String) { diff --git a/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs index 4fb5a379e..1b409882f 100644 --- a/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure.Json writer.WriteValue($"{value.Id},{value.Name}"); } - protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.String) { diff --git a/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs b/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs index fad3d3243..ae6990e9a 100644 --- a/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs +++ b/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs @@ -13,9 +13,9 @@ using NodaTime.Extensions; namespace Squidex.Infrastructure.Json { - public sealed class PropertiesBagConverter : JsonClassConverter + public sealed class PropertiesBagConverter : JsonClassConverter where T : PropertiesBag, new() { - protected override void WriteValue(JsonWriter writer, PropertiesBag value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, T value, JsonSerializer serializer) { writer.WriteStartObject(); @@ -36,14 +36,14 @@ namespace Squidex.Infrastructure.Json writer.WriteEndObject(); } - protected override PropertiesBag ReadValue(JsonReader reader, JsonSerializer serializer) + protected override T ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.StartObject) { throw new JsonException($"Expected Object, but got {reader.TokenType}."); } - var properties = new PropertiesBag(); + var properties = new T(); while (reader.Read()) { @@ -73,7 +73,7 @@ namespace Squidex.Infrastructure.Json public override bool CanConvert(Type objectType) { - return typeof(PropertiesBag).IsAssignableFrom(objectType); + return objectType == typeof(T); } } } diff --git a/src/Squidex.Infrastructure/Json/RefTokenConverter.cs b/src/Squidex.Infrastructure/Json/RefTokenConverter.cs index 9964c7f3a..92dfa7a4c 100644 --- a/src/Squidex.Infrastructure/Json/RefTokenConverter.cs +++ b/src/Squidex.Infrastructure/Json/RefTokenConverter.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json @@ -17,7 +18,7 @@ namespace Squidex.Infrastructure.Json writer.WriteValue(value.ToString()); } - protected override RefToken ReadValue(JsonReader reader, JsonSerializer serializer) + protected override RefToken ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { if (reader.TokenType != JsonToken.String) { diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs index b78102e4f..f4c6114d2 100644 --- a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs +++ b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs @@ -20,11 +20,11 @@ namespace Squidex.Infrastructure.Log.Adapter return builder; } - public static LoggerFactory AddSemanticLog(this LoggerFactory builder, ISemanticLog log) + public static ILoggerFactory AddSemanticLog(this ILoggerFactory factory, ISemanticLog log) { - builder.AddProvider(new SemanticLogLoggerProvider(log)); + factory.AddProvider(new SemanticLogLoggerProvider(log)); - return builder; + return factory; } } } diff --git a/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs b/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs index f64a2253f..5acce2310 100644 --- a/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs +++ b/src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs @@ -22,7 +22,18 @@ namespace Squidex.Infrastructure.Log public void Log(SemanticLogLevel logLevel, string message) { - processor.EnqueueMessage(new LogMessageEntry { Message = message, IsError = logLevel >= SemanticLogLevel.Error }); + var color = 0; + + if (logLevel == SemanticLogLevel.Warning) + { + color = 0xffff00; + } + else if (logLevel >= SemanticLogLevel.Error) + { + color = 0xff0000; + } + + processor.EnqueueMessage(new LogMessageEntry { Message = message, Color = color }); } } } diff --git a/src/Squidex.Infrastructure/Log/FileChannel.cs b/src/Squidex.Infrastructure/Log/FileChannel.cs index 885d0380a..bf70e22ee 100644 --- a/src/Squidex.Infrastructure/Log/FileChannel.cs +++ b/src/Squidex.Infrastructure/Log/FileChannel.cs @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.Log public void Log(SemanticLogLevel logLevel, string message) { - processor.EnqueueMessage(new LogMessageEntry { Message = message, IsError = logLevel >= SemanticLogLevel.Error }); + processor.EnqueueMessage(new LogMessageEntry { Message = message }); } public void Connect() diff --git a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs b/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs index b476e5834..77535fc1d 100644 --- a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs +++ b/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs @@ -19,9 +19,9 @@ namespace Squidex.Infrastructure.Log.Internal this.logToStdError = logToStdError; } - public void WriteLine(bool isError, string message) + public void WriteLine(int color, string message) { - if (isError && logToStdError) + if (color != 0 && logToStdError) { Console.Error.WriteLine(message); } diff --git a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs b/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs index 50465f29a..af582c278 100644 --- a/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs +++ b/src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs @@ -8,12 +8,13 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace Squidex.Infrastructure.Log.Internal { - public class ConsoleLogProcessor : DisposableObjectBase + public sealed class ConsoleLogProcessor : DisposableObjectBase { private const int MaxQueuedMessages = 1024; private readonly IConsole console; @@ -24,7 +25,7 @@ namespace Squidex.Infrastructure.Log.Internal { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - console = new WindowsLogConsole(false); + console = new WindowsLogConsole(true); } else { @@ -36,22 +37,40 @@ namespace Squidex.Infrastructure.Log.Internal public void EnqueueMessage(LogMessageEntry message) { - messageQueue.Add(message); + if (!messageQueue.IsAddingCompleted) + { + try + { + messageQueue.Add(message); + return; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to enqueue log message: {ex}."); + } + } + + WriteMessage(message); + } + + private static void ProcessLogQueue(object state) + { + var processor = (ConsoleLogProcessor)state; + + processor.ProcessLogQueue(); } private void ProcessLogQueue() { foreach (var entry in messageQueue.GetConsumingEnumerable()) { - console.WriteLine(entry.IsError, entry.Message); + WriteMessage(entry); } } - private static void ProcessLogQueue(object state) + private void WriteMessage(LogMessageEntry entry) { - var processor = (ConsoleLogProcessor)state; - - processor.ProcessLogQueue(); + console.WriteLine(entry.Color, entry.Message); } protected override void DisposeObject(bool disposing) @@ -59,7 +78,6 @@ namespace Squidex.Infrastructure.Log.Internal if (disposing) { messageQueue.CompleteAdding(); - messageQueue.Dispose(); try { @@ -67,10 +85,7 @@ namespace Squidex.Infrastructure.Log.Internal } catch (Exception ex) { - if (!ex.Is()) - { - throw; - } + Debug.WriteLine($"Failed to shutdown log queue grateful: {ex}."); } } } diff --git a/src/Squidex.Infrastructure/Log/Internal/IConsole.cs b/src/Squidex.Infrastructure/Log/Internal/IConsole.cs index 6186d4a60..f9808c13d 100644 --- a/src/Squidex.Infrastructure/Log/Internal/IConsole.cs +++ b/src/Squidex.Infrastructure/Log/Internal/IConsole.cs @@ -10,6 +10,6 @@ namespace Squidex.Infrastructure.Log.Internal { public interface IConsole { - void WriteLine(bool isError, string message); + void WriteLine(int color, string message); } } diff --git a/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs b/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs index 5c506116e..418343967 100644 --- a/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs +++ b/src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs @@ -10,7 +10,7 @@ namespace Squidex.Infrastructure.Log.Internal { public struct LogMessageEntry { - public bool IsError; + public int Color; public string Message; } diff --git a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs b/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs index e39481ab8..658b5a352 100644 --- a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs +++ b/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs @@ -19,13 +19,20 @@ namespace Squidex.Infrastructure.Log.Internal this.logToStdError = logToStdError; } - public void WriteLine(bool isError, string message) + public void WriteLine(int color, string message) { - if (isError) + if (color != 0) { try { - Console.ForegroundColor = ConsoleColor.Red; + if (color == 0xffff00) + { + Console.ForegroundColor = ConsoleColor.Yellow; + } + else + { + Console.ForegroundColor = ConsoleColor.Red; + } if (logToStdError) { diff --git a/src/Squidex.Infrastructure/PubSubExtensions.cs b/src/Squidex.Infrastructure/PubSubExtensions.cs new file mode 100644 index 000000000..b1669a341 --- /dev/null +++ b/src/Squidex.Infrastructure/PubSubExtensions.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// PubSubExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +#pragma warning disable 4014 +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + +namespace Squidex.Infrastructure +{ + public static class PubSubExtensions + { + public class Request + { + public T Body { get; set; } + + public Guid CorrelationId { get; set; } + } + + public class Response + { + public T Body { get; set; } + + public Guid CorrelationId { get; set; } + } + + public static IDisposable ReceiveAsync(this IPubSub pubsub, Func> callback, bool self = true) + { + return pubsub.Subscribe>(async x => + { + var response = await callback(x.Body); + + pubsub.Publish(new Response { CorrelationId = x.CorrelationId, Body = response }, true); + }); + } + + public static async Task RequestAsync(this IPubSub pubsub, TRequest message, TimeSpan timeout, bool self = true) + { + var request = new Request { Body = message, CorrelationId = Guid.NewGuid() }; + + IDisposable subscription = null; + try + { + var receiveTask = new TaskCompletionSource(); + + subscription = pubsub.Subscribe>(response => + { + if (response.CorrelationId == request.CorrelationId) + { + receiveTask.SetResult(response.Body); + } + }); + + Task.Run(() => pubsub.Publish(request, self)); + + var firstTask = await Task.WhenAny(receiveTask.Task, Task.Delay(timeout)); + + if (firstTask.Id != receiveTask.Task.Id) + { + throw new TaskCanceledException(); + } + else + { + return await receiveTask.Task; + } + } + finally + { + subscription?.Dispose(); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs index 9cde81872..3f9f65703 100644 --- a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs +++ b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs @@ -64,5 +64,10 @@ namespace Squidex.Infrastructure.Reflection return flattenProperties.ToArray(); } + + public static bool Implements(this Type type) + { + return type.GetInterfaces().Contains(typeof(T)); + } } } diff --git a/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs new file mode 100644 index 000000000..25ddf162c --- /dev/null +++ b/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// SimpleCopier.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; + +#pragma warning disable RECS0108 // Warns about static fields in generic types + +namespace Squidex.Infrastructure.Reflection +{ + public static class SimpleCopier + { + private struct PropertyMapper + { + private readonly IPropertyAccessor accessor; + private readonly Func converter; + + public PropertyMapper(IPropertyAccessor accessor, Func converter) + { + this.accessor = accessor; + this.converter = converter; + } + + public void MapProperty(object source, object target) + { + var value = converter(accessor.Get(source)); + + accessor.Set(target, value); + } + } + + private static class ClassCopier where T : class, new() + { + private static readonly List Mappers = new List(); + + static ClassCopier() + { + var type = typeof(T); + + foreach (var property in type.GetPublicProperties()) + { + if (!property.CanWrite || !property.CanRead) + { + continue; + } + + var accessor = new PropertyAccessor(type, property); + + if (property.PropertyType.Implements()) + { + Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x)?.Clone())); + } + else + { + Mappers.Add(new PropertyMapper(accessor, x => x)); + } + } + } + + public static T CopyThis(T source) + { + var destination = new T(); + + foreach (var mapper in Mappers) + { + mapper.MapProperty(source, destination); + } + + return destination; + } + } + + public static T Copy(this T source) where T : class, new() + { + Guard.NotNull(source, nameof(source)); + + return ClassCopier.CopyThis(source); + } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs index 065e3e26d..dabd13661 100644 --- a/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ b/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -21,13 +21,16 @@ namespace Squidex.Infrastructure.Reflection { private readonly Type targetType; - public ConversionPropertyMapper(IPropertyAccessor srcAccessor, IPropertyAccessor dstAccessor, Type targetType) - : base(srcAccessor, dstAccessor) + public ConversionPropertyMapper( + IPropertyAccessor sourceAccessor, + IPropertyAccessor targetAccessor, + Type targetType) + : base(sourceAccessor, targetAccessor) { this.targetType = targetType; } - public override void MapProperty(object source, object destination, CultureInfo culture) + public override void MapProperty(object source, object target, CultureInfo culture) { var value = GetValue(source); @@ -41,7 +44,7 @@ namespace Squidex.Infrastructure.Reflection { converted = Convert.ChangeType(value, targetType, culture); - SetValue(destination, converted); + SetValue(target, converted); } catch (InvalidCastException) { @@ -49,7 +52,7 @@ namespace Squidex.Infrastructure.Reflection { converted = value.ToString(); - SetValue(destination, converted); + SetValue(target, converted); } } } @@ -57,91 +60,78 @@ namespace Squidex.Infrastructure.Reflection private class PropertyMapper { - private readonly IPropertyAccessor srcAccessor; - private readonly IPropertyAccessor dstAccessor; + private readonly IPropertyAccessor sourceAccessor; + private readonly IPropertyAccessor targetAccessor; - public PropertyMapper(IPropertyAccessor srcAccessor, IPropertyAccessor dstAccessor) + public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) { - this.srcAccessor = srcAccessor; - this.dstAccessor = dstAccessor; + this.sourceAccessor = sourceAccessor; + this.targetAccessor = targetAccessor; } - public virtual void MapProperty(object source, object destination, CultureInfo culture) + public virtual void MapProperty(object source, object target, CultureInfo culture) { var value = GetValue(source); - SetValue(destination, value); + SetValue(target, value); } protected void SetValue(object destination, object value) { - dstAccessor.Set(destination, value); + targetAccessor.Set(destination, value); } protected object GetValue(object source) { - return srcAccessor.Get(source); + return sourceAccessor.Get(source); } } - private static class ClassMapper - where TSource : class - where TDestination : class + private static class ClassMapper where TSource : class where TTarget : class { - private static readonly PropertyMapper[] Mappers; - - private static readonly Type[] Convertibles = - { - typeof(bool), - typeof(byte), - typeof(char), - typeof(decimal), - typeof(float), - typeof(double), - typeof(short), - typeof(int), - typeof(long), - typeof(string) - }; + private static readonly List Mappers = new List(); static ClassMapper() { - var dstType = typeof(TDestination); - var srcType = typeof(TSource); + var sourceClassType = typeof(TSource); + var sourceProperties = + sourceClassType.GetPublicProperties() + .Where(x => x.CanRead).ToList(); - var destinationProperties = dstType.GetPublicProperties(); + var targetClassType = typeof(TTarget); + var targetProperties = + targetClassType.GetPublicProperties() + .Where(x => x.CanWrite).ToList(); - var newMappers = new List(); - - foreach (var srcProperty in srcType.GetPublicProperties().Where(x => x.CanRead)) + foreach (var sourceProperty in sourceProperties) { - var dstProperty = destinationProperties.FirstOrDefault(x => x.Name == srcProperty.Name); + var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); - if (dstProperty == null || !dstProperty.CanWrite) + if (targetProperty == null) { continue; } - var srcPropertyType = srcProperty.PropertyType; - var dstPropertyType = dstProperty.PropertyType; + var sourceType = sourceProperty.PropertyType; + var targetType = targetProperty.PropertyType; - if (srcPropertyType == dstPropertyType) + if (sourceType == targetType) { - newMappers.Add(new PropertyMapper(new PropertyAccessor(srcType, srcProperty), new PropertyAccessor(dstType, dstProperty))); + Mappers.Add(new PropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty))); } - else + else if (targetType.Implements()) { - if (Convertibles.Contains(dstPropertyType)) - { - newMappers.Add(new ConversionPropertyMapper(new PropertyAccessor(srcType, srcProperty), new PropertyAccessor(dstType, dstProperty), dstPropertyType)); - } + Mappers.Add(new ConversionPropertyMapper( + new PropertyAccessor(sourceClassType, sourceProperty), + new PropertyAccessor(targetClassType, targetProperty), + targetType)); } } - - Mappers = newMappers.ToArray(); } - public static TDestination MapClass(TSource source, TDestination destination, CultureInfo culture) + public static TTarget MapClass(TSource source, TTarget destination, CultureInfo culture) { foreach (var mapper in Mappers) { @@ -152,29 +142,29 @@ namespace Squidex.Infrastructure.Reflection } } - public static TDestination Map(TSource source) + public static TTarget Map(TSource source) where TSource : class - where TDestination : class, new() + where TTarget : class, new() { - return Map(source, new TDestination(), CultureInfo.CurrentCulture); + return Map(source, new TTarget(), CultureInfo.CurrentCulture); } - public static TDestination Map(TSource source, TDestination destination) + public static TTarget Map(TSource source, TTarget target) where TSource : class - where TDestination : class + where TTarget : class { - return Map(source, destination, CultureInfo.CurrentCulture); + return Map(source, target, CultureInfo.CurrentCulture); } - public static TDestination Map(TSource source, TDestination destination, CultureInfo culture) + public static TTarget Map(TSource source, TTarget target, CultureInfo culture) where TSource : class - where TDestination : class + where TTarget : class { Guard.NotNull(source, nameof(source)); Guard.NotNull(culture, nameof(culture)); - Guard.NotNull(destination, nameof(destination)); + Guard.NotNull(target, nameof(target)); - return ClassMapper.MapClass(source, destination, culture); + return ClassMapper.MapClass(source, target, culture); } } } diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index c338c9865..0e7de800d 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs new file mode 100644 index 000000000..1c238f0ed --- /dev/null +++ b/src/Squidex.Infrastructure/States/IStateFactory.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// IStateFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.States +{ + public interface IStateFactory + { + Task GetAsync(string key) where T : StatefulObject; + } +} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppProvider.cs b/src/Squidex.Infrastructure/States/IStateHolder.cs similarity index 61% rename from src/Squidex.Domain.Apps.Read/Apps/Services/IAppProvider.cs rename to src/Squidex.Infrastructure/States/IStateHolder.cs index 632dd46e4..b2425ffe5 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppProvider.cs +++ b/src/Squidex.Infrastructure/States/IStateHolder.cs @@ -1,20 +1,21 @@ // ========================================================================== -// IAppProvider.cs +// IStateHolder.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; using System.Threading.Tasks; -namespace Squidex.Domain.Apps.Read.Apps.Services +namespace Squidex.Infrastructure.States { - public interface IAppProvider + public interface IStateHolder { - Task FindAppByIdAsync(Guid id); + T State { get; set; } - Task FindAppByNameAsync(string name); + Task ReadAsync(); + + Task WriteAsync(); } } diff --git a/src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs b/src/Squidex.Infrastructure/States/IStateStore.cs similarity index 59% rename from src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs rename to src/Squidex.Infrastructure/States/IStateStore.cs index 260a9d2ff..f6f903b95 100644 --- a/src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs +++ b/src/Squidex.Infrastructure/States/IStateStore.cs @@ -1,20 +1,19 @@ // ========================================================================== -// IRemoteActorChannel.cs +// IStateStore.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System; using System.Threading.Tasks; -namespace Squidex.Infrastructure.Actors +namespace Squidex.Infrastructure.States { - public interface IRemoteActorChannel + public interface IStateStore { - Task SendAsync(string recipient, object message); + Task WriteAsync(string key, T value, string oldEtag, string newEtag); - void Subscribe(string recipient, Action handler); + Task<(T Value, string Etag)> ReadAsync(string key); } -} \ No newline at end of file +} diff --git a/src/Squidex.Infrastructure/States/InconsistentStateException.cs b/src/Squidex.Infrastructure/States/InconsistentStateException.cs new file mode 100644 index 000000000..236919152 --- /dev/null +++ b/src/Squidex.Infrastructure/States/InconsistentStateException.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// InconsistentStateException.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Runtime.Serialization; + +namespace Squidex.Infrastructure.States +{ + [Serializable] + public class InconsistentStateException : Exception + { + private readonly string currentEtag; + private readonly string expectedEtag; + + public string CurrentEtag + { + get { return currentEtag; } + } + + public string ExpectedEtag + { + get { return expectedEtag; } + } + + public InconsistentStateException(string currentEtag, string expectedEtag, Exception ex) + : base(FormatMessage(currentEtag, expectedEtag), ex) + { + this.currentEtag = currentEtag; + + this.expectedEtag = expectedEtag; + } + + protected InconsistentStateException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string currentEtag, string expectedEtag) + { + return $"Requested etag {expectedEtag}, but found {currentEtag}."; + } + } +} diff --git a/src/Squidex.Infrastructure/Actors/IActor.cs b/src/Squidex.Infrastructure/States/InvalidateMessage.cs similarity index 69% rename from src/Squidex.Infrastructure/Actors/IActor.cs rename to src/Squidex.Infrastructure/States/InvalidateMessage.cs index 7975a90ab..56aa270a4 100644 --- a/src/Squidex.Infrastructure/Actors/IActor.cs +++ b/src/Squidex.Infrastructure/States/InvalidateMessage.cs @@ -1,15 +1,15 @@ // ========================================================================== -// IActor.cs +// InvalidateMessage.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.Actors +namespace Squidex.Infrastructure.States { - public interface IActor + public sealed class InvalidateMessage { - void Tell(object message); + public string Key { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs new file mode 100644 index 000000000..1e7f282c5 --- /dev/null +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -0,0 +1,120 @@ +// ========================================================================== +// StateFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.States +{ + public sealed class StateFactory : DisposableObjectBase, IExternalSystem, IStateFactory + { + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); + private readonly IPubSub pubSub; + private readonly IStateStore store; + private readonly IMemoryCache statesCache; + private readonly IServiceProvider services; + private readonly List states = new List(); + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); + private IDisposable pubSubscription; + + public StateFactory( + IPubSub pubSub, + IServiceProvider services, + IStateStore store, + IMemoryCache statesCache) + { + Guard.NotNull(pubSub, nameof(pubSub)); + Guard.NotNull(store, nameof(store)); + Guard.NotNull(services, nameof(services)); + Guard.NotNull(statesCache, nameof(statesCache)); + + this.pubSub = pubSub; + this.store = store; + this.services = services; + this.statesCache = statesCache; + } + + public void Connect() + { + pubSubscription = pubSub.Subscribe(m => + { + statesCache.Remove(m.Key); + }); + } + + public Task GetAsync(string key) where T : StatefulObject + { + Guard.NotNull(key, nameof(key)); + + var tcs = new TaskCompletionSource(); + + dispatcher.DispatchAsync(async () => + { + try + { + if (statesCache.TryGetValue(key, out var state)) + { + tcs.SetResult(state); + } + else + { + state = (T)services.GetService(typeof(T)); + + var stateHolder = new StateHolder(key, () => + { + pubSub.Publish(new InvalidateMessage { Key = key }, false); + }, store); + + await state.ActivateAsync(stateHolder); + + statesCache.CreateEntry(key) + .SetValue(state) + .SetAbsoluteExpiration(CacheDuration) + .RegisterPostEvictionCallback((k, v, r, s) => + { + dispatcher.DispatchAsync(() => + { + state.Dispose(); + states.Remove(state); + }).Forget(); + }) + .Dispose(); + + states.Add(state); + + tcs.SetResult(state); + } + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + dispatcher.DispatchAsync(() => + { + foreach (var state in states) + { + state.Dispose(); + } + }); + dispatcher.StopAndWaitAsync().Wait(); + } + } + } +} diff --git a/src/Squidex.Infrastructure/States/StateHolder.cs b/src/Squidex.Infrastructure/States/StateHolder.cs new file mode 100644 index 000000000..098142ea6 --- /dev/null +++ b/src/Squidex.Infrastructure/States/StateHolder.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// StateHolder.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.States +{ + public sealed class StateHolder : IStateHolder + { + private readonly Action written; + private readonly IStateStore store; + private readonly string key; + private string etag; + + public T State { get; set; } + + public StateHolder(string key, Action written, IStateStore store) + { + this.key = key; + this.store = store; + this.written = written; + } + + public async Task ReadAsync() + { + (State, etag) = await store.ReadAsync(key); + + if (Equals(State, default(T))) + { + State = Activator.CreateInstance(); + } + } + + public async Task WriteAsync() + { + var newEtag = Guid.NewGuid().ToString(); + + await store.WriteAsync(key, State, etag, newEtag); + + etag = newEtag; + } + } +} diff --git a/src/Squidex.Infrastructure/States/StatefulObject.cs b/src/Squidex.Infrastructure/States/StatefulObject.cs new file mode 100644 index 000000000..b384c09e4 --- /dev/null +++ b/src/Squidex.Infrastructure/States/StatefulObject.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// StatefulActor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.States +{ + public abstract class StatefulObject : DisposableObjectBase + { + private IStateHolder stateHolder; + + public T State + { + get + { + if (stateHolder != null) + { + return stateHolder.State; + } + else + { + return default(T); + } + } + + protected set + { + if (stateHolder != null) + { + stateHolder.State = value; + } + } + } + + public Task ActivateAsync(IStateHolder stateHolder) + { + Guard.NotNull(stateHolder, nameof(stateHolder)); + + this.stateHolder = stateHolder; + + return stateHolder.ReadAsync(); + } + + public virtual async Task ReadStateAsync() + { + if (stateHolder != null) + { + await stateHolder.ReadAsync(); + } + } + + public virtual async Task WriteStateAsync() + { + if (stateHolder != null) + { + await stateHolder.WriteAsync(); + } + } + + protected override void DisposeObject(bool disposing) + { + } + } +} diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs index 8279a401a..8fd004668 100644 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ b/src/Squidex.Infrastructure/StringExtensions.cs @@ -328,7 +328,7 @@ namespace Squidex.Infrastructure { var sb = new StringBuilder(); - foreach (var part in value.Split(new[] { '-', '_', ' ' })) + foreach (var part in value.Split('-', '_', ' ')) { if (part.Length < 2) { diff --git a/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs b/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs similarity index 55% rename from src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs rename to src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs index 993ae84f1..378bfc19c 100644 --- a/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs +++ b/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs @@ -9,27 +9,72 @@ using System; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; -using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.Actors +namespace Squidex.Infrastructure.Tasks { public sealed class SingleThreadedDispatcher { private readonly ActionBlock> block; private bool isStopped; - public SingleThreadedDispatcher(int capacity = 10) + public SingleThreadedDispatcher(int capacity = 1) { var options = new ExecutionDataflowBlockOptions { + BoundedCapacity = capacity, MaxMessagesPerTask = -1, - MaxDegreeOfParallelism = 1, - BoundedCapacity = capacity + MaxDegreeOfParallelism = 1 }; block = new ActionBlock>(Handle, options); } + public Task DispatchAndUnwrapAsync(Func action) + { + Guard.NotNull(action, nameof(action)); + + var tcs = new TaskCompletionSource(); + + block.SendAsync(async () => + { + try + { + await action(); + + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; + } + + public Task DispatchAndUnwrapAsync(Func> action) + { + Guard.NotNull(action, nameof(action)); + + var tcs = new TaskCompletionSource(); + + block.SendAsync(async () => + { + try + { + var result = await action(); + + tcs.SetResult(result); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; + } + public Task DispatchAsync(Func action) { Guard.NotNull(action, nameof(action)); diff --git a/src/Squidex.Infrastructure/TypeNameNotFoundException.cs b/src/Squidex.Infrastructure/TypeNameNotFoundException.cs index 35c5db9ff..520351477 100644 --- a/src/Squidex.Infrastructure/TypeNameNotFoundException.cs +++ b/src/Squidex.Infrastructure/TypeNameNotFoundException.cs @@ -7,9 +7,11 @@ // ========================================================================== using System; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class TypeNameNotFoundException : Exception { public TypeNameNotFoundException() @@ -25,5 +27,10 @@ namespace Squidex.Infrastructure : base(message, inner) { } + + protected TypeNameNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/src/Squidex.Infrastructure/ValidationException.cs b/src/Squidex.Infrastructure/ValidationException.cs index 637021487..71034ed5b 100644 --- a/src/Squidex.Infrastructure/ValidationException.cs +++ b/src/Squidex.Infrastructure/ValidationException.cs @@ -9,9 +9,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; namespace Squidex.Infrastructure { + [Serializable] public class ValidationException : Exception { private static readonly List FallbackErrors = new List(); @@ -46,6 +48,11 @@ namespace Squidex.Infrastructure this.errors = errors ?? FallbackErrors; } + protected ValidationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + public override string ToString() { return string.Join(" ", Enumerable.Repeat(Message, 1).Union(Errors.Select(x => x.Message))); diff --git a/src/Squidex.Shared/Squidex.Shared.csproj b/src/Squidex.Shared/Squidex.Shared.csproj index 30976c0d8..7a978dff4 100644 --- a/src/Squidex.Shared/Squidex.Shared.csproj +++ b/src/Squidex.Shared/Squidex.Shared.csproj @@ -7,7 +7,7 @@ True - + diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs index 05d969b3f..88a370b5b 100644 --- a/src/Squidex/AppServices.cs +++ b/src/Squidex/AppServices.cs @@ -8,10 +8,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Squidex.Areas.Api.Config.Swagger; +using Squidex.Areas.IdentityServer.Config; using Squidex.Config; +using Squidex.Config.Authentication; using Squidex.Config.Domain; -using Squidex.Config.Identity; -using Squidex.Config.Swagger; using Squidex.Config.Web; namespace Squidex @@ -26,10 +27,8 @@ namespace Squidex services.AddMyAssetServices(config); services.AddMyAuthentication(config); - services.AddMyDataProtectection(config); services.AddMyEventPublishersServices(config); services.AddMyEventStoreServices(config); - services.AddMyIdentity(); services.AddMyIdentityServer(); services.AddMyInfrastructureServices(config); services.AddMyMvc(); diff --git a/src/Squidex/Config/Swagger/ScopesProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs similarity index 96% rename from src/Squidex/Config/Swagger/ScopesProcessor.cs rename to src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs index 57e9a3b2f..71c1f3a29 100644 --- a/src/Squidex/Config/Swagger/ScopesProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs @@ -14,9 +14,10 @@ using Microsoft.AspNetCore.Authorization; using NSwag; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using Squidex.Config; using Squidex.Infrastructure.Tasks; -namespace Squidex.Config.Swagger +namespace Squidex.Areas.Api.Config.Swagger { public class ScopesProcessor : IOperationProcessor { diff --git a/src/Squidex/Config/Swagger/SwaggerExtensions.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs similarity index 94% rename from src/Squidex/Config/Swagger/SwaggerExtensions.cs rename to src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs index c15976c81..ce022db94 100644 --- a/src/Squidex/Config/Swagger/SwaggerExtensions.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerExtensions.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using NSwag.AspNetCore; -namespace Squidex.Config.Swagger +namespace Squidex.Areas.Api.Config.Swagger { public static class SwaggerExtensions { diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs similarity index 87% rename from src/Squidex/Config/Swagger/SwaggerServices.cs rename to src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index c3d4346ad..d34d09d7b 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -13,12 +13,13 @@ using NJsonSchema; using NJsonSchema.Generation.TypeMappers; using NodaTime; using NSwag.AspNetCore; -using NSwag.SwaggerGeneration.WebApi.Processors.Security; -using Squidex.Controllers.ContentApi.Generator; +using NSwag.SwaggerGeneration.Processors.Security; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Config; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; -namespace Squidex.Config.Swagger +namespace Squidex.Areas.Api.Config.Swagger { public static class SwaggerServices { @@ -29,7 +30,7 @@ namespace Squidex.Config.Swagger var urlOptions = s.GetService>().Value; var settings = - new SwaggerSettings { Title = "Squidex API Specification", IsAspNetCore = false } + new SwaggerSettings { Title = "Squidex API", Version = "1.0", IsAspNetCore = false } .ConfigurePaths(urlOptions) .ConfigureSchemaSettings() .ConfigureIdentity(urlOptions); @@ -42,7 +43,9 @@ namespace Squidex.Config.Swagger private static SwaggerSettings ConfigureIdentity(this SwaggerSettings settings, MyUrlsOptions urlOptions) { - settings.DocumentProcessors.Add(new SecurityDefinitionAppender(Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions))); + settings.DocumentProcessors.Add( + new SecurityDefinitionAppender( + Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions))); settings.OperationProcessors.Add(new ScopesProcessor()); diff --git a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs similarity index 96% rename from src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs rename to src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs index 6c239444f..0242c3891 100644 --- a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs @@ -17,11 +17,11 @@ using Squidex.Pipeline.Swagger; #pragma warning disable RECS0033 // Convert 'if' to '||' expression -namespace Squidex.Config.Swagger +namespace Squidex.Areas.Api.Config.Swagger { public sealed class XmlResponseTypesProcessor : IOperationProcessor { - private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); + private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); public async Task ProcessAsync(OperationProcessorContext context) { diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs similarity index 98% rename from src/Squidex/Config/Swagger/XmlTagProcessor.cs rename to src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs index 0e9b79f6e..93b24b7f2 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs @@ -14,7 +14,7 @@ using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; using Squidex.Infrastructure.Tasks; -namespace Squidex.Config.Swagger +namespace Squidex.Areas.Api.Config.Swagger { public sealed class XmlTagProcessor : IOperationProcessor, IDocumentProcessor { diff --git a/src/Squidex/Controllers/ControllerBase.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs similarity index 68% rename from src/Squidex/Controllers/ControllerBase.cs rename to src/Squidex/Areas/Api/Controllers/ApiController.cs index db2c972e1..eaca70e1f 100644 --- a/src/Squidex/Controllers/ControllerBase.cs +++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs @@ -8,24 +8,19 @@ using System; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Domain.Apps.Read.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers +namespace Squidex.Areas.Api.Controllers { - public abstract class ControllerBase : Controller + [Area("Api")] + public abstract class ApiController : Controller { protected ICommandBus CommandBus { get; } - protected ControllerBase(ICommandBus commandBus) - { - Guard.NotNull(commandBus, nameof(commandBus)); - - CommandBus = commandBus; - } - protected IAppEntity App { get @@ -41,11 +36,23 @@ namespace Squidex.Controllers } } - protected Guid AppId + protected string AppName { - get + get { return App.Name; } + } + + protected ApiController(ICommandBus commandBus) + { + Guard.NotNull(commandBus, nameof(commandBus)); + + CommandBus = commandBus; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.HttpContext.Request.PathBase.StartsWithSegments("/api")) { - return App.Id; + context.Result = new RedirectResult("/"); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs similarity index 96% rename from src/Squidex/Controllers/Api/Apps/AppClientsController.cs rename to src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index ed388de82..1b97323e7 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -11,13 +11,13 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Apps.Models; +using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Apps +namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. @@ -25,9 +25,9 @@ namespace Squidex.Controllers.Api.Apps [ApiAuthorize] [ApiExceptionFilter] [AppApi] - [MustBeAppOwner] + [MustBeAppEditor] [SwaggerTag(nameof(Apps))] - public sealed class AppClientsController : ControllerBase + public sealed class AppClientsController : ApiController { public AppClientsController(ICommandBus commandBus) : base(commandBus) diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs similarity index 95% rename from src/Squidex/Controllers/Api/Apps/AppContributorsController.cs rename to src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index b6acd8d12..79830a43b 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -11,14 +11,14 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Apps.Models; +using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Apps +namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. @@ -28,7 +28,7 @@ namespace Squidex.Controllers.Api.Apps [AppApi] [MustBeAppOwner] [SwaggerTag(nameof(Apps))] - public sealed class AppContributorsController : ControllerBase + public sealed class AppContributorsController : ApiController { private readonly IAppPlansProvider appPlansProvider; diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs similarity index 95% rename from src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs rename to src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 4d0ee698a..fc9562994 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Apps.Models; +using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; @@ -21,7 +21,7 @@ using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Apps +namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. @@ -30,7 +30,7 @@ namespace Squidex.Controllers.Api.Apps [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Apps))] - public sealed class AppLanguagesController : ControllerBase + public sealed class AppLanguagesController : ApiController { public AppLanguagesController(ICommandBus commandBus) : base(commandBus) @@ -75,7 +75,7 @@ namespace Squidex.Controllers.Api.Apps /// 400 => Language is an invalid language. /// 404 => App not found. /// - [MustBeAppOwner] + [MustBeAppEditor] [HttpPost] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguageDto), 201)] @@ -101,7 +101,7 @@ namespace Squidex.Controllers.Api.Apps /// 400 => Language object is invalid. /// 404 => App not found. /// - [MustBeAppOwner] + [MustBeAppEditor] [HttpPut] [Route("apps/{app}/languages/{language}/")] [ApiCosts(1)] @@ -121,7 +121,7 @@ namespace Squidex.Controllers.Api.Apps /// 204 => Language deleted. /// 404 => App not found. /// - [MustBeAppOwner] + [MustBeAppEditor] [HttpDelete] [Route("apps/{app}/languages/{language}/")] [ApiCosts(1)] diff --git a/src/Squidex/Controllers/Api/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs similarity index 90% rename from src/Squidex/Controllers/Api/Apps/AppsController.cs rename to src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 31ddaa735..5b8c2d6c5 100644 --- a/src/Squidex/Controllers/Api/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -11,9 +11,9 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.Apps.Models; +using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps.Repositories; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.CQRS.Commands; @@ -21,7 +21,7 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Apps +namespace Squidex.Areas.Api.Controllers.Apps { /// /// Manages and configures apps. @@ -29,17 +29,17 @@ namespace Squidex.Controllers.Api.Apps [ApiAuthorize] [ApiExceptionFilter] [SwaggerTag(nameof(Apps))] - public sealed class AppsController : ControllerBase + public sealed class AppsController : ApiController { - private readonly IAppRepository appRepository; + private readonly IAppProvider appProvider; private readonly IAppPlansProvider appPlansProvider; public AppsController(ICommandBus commandBus, - IAppRepository appRepository, + IAppProvider appProvider, IAppPlansProvider appPlansProvider) : base(commandBus) { - this.appRepository = appRepository; + this.appProvider = appProvider; this.appPlansProvider = appPlansProvider; } @@ -61,7 +61,7 @@ namespace Squidex.Controllers.Api.Apps { var subject = HttpContext.User.OpenIdSubject(); - var apps = await appRepository.QueryAllAsync(subject); + var apps = await appProvider.GetUserApps(subject); var response = apps.Select(a => { diff --git a/src/Squidex/Controllers/Api/Apps/Models/AddAppLanguageDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Apps/Models/AddAppLanguageDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs index d604c8aac..fc1a928ba 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AddAppLanguageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs @@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AddAppLanguageDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs index 90c191755..30b3de2c9 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AppCreatedDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Apps/Models/AppDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index f48917dae..dbc6e513e 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -13,7 +13,7 @@ using Newtonsoft.Json.Converters; using NodaTime; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AppDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs index 40ff4771c..90a9699af 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AppLanguageDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/AssignAppContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Apps/Models/AssignAppContributorDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs index 9f2480ddd..8083dd614 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AssignAppContributorDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class AssignAppContributorDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs index 90f74e701..059ee5918 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class ClientDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/ContributorDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Apps/Models/ContributorDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs index dd5bed79b..1892bfb29 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ContributorDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class ContributorDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/ContributorsDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Apps/Models/ContributorsDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs index cd542674b..08bd5e252 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ContributorsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class ContributorsDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/CreateAppClientDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Apps/Models/CreateAppClientDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs index 91ccf179f..c6df27c98 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/CreateAppClientDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class CreateAppClientDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/CreateAppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Apps/Models/CreateAppDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs index f9b0ab99d..61361ea8b 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/CreateAppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class CreateAppDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs index 2140f52ef..02b718bdf 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core.Apps; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class UpdateAppClientDto { diff --git a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs index d2d21cc52..05454fbe6 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { public sealed class UpdateAppLanguageDto { diff --git a/src/Squidex/Controllers/Api/Assets/AssetConfig.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetConfig.cs similarity index 90% rename from src/Squidex/Controllers/Api/Assets/AssetConfig.cs rename to src/Squidex/Areas/Api/Controllers/Assets/AssetConfig.cs index 1e4ec5a14..37a9b72f4 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetConfig.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetConfig.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Assets +namespace Squidex.Areas.Api.Controllers.Assets { public sealed class AssetConfig { diff --git a/src/Squidex/Controllers/Api/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs similarity index 94% rename from src/Squidex/Controllers/Api/Assets/AssetContentController.cs rename to src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index daec1cdd4..e02d66871 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -18,7 +18,7 @@ using Squidex.Pipeline; #pragma warning disable 1573 -namespace Squidex.Controllers.Api.Assets +namespace Squidex.Areas.Api.Controllers.Assets { /// /// Uploads and retrieves assets. @@ -26,7 +26,7 @@ namespace Squidex.Controllers.Api.Assets [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Assets))] - public sealed class AssetContentController : ControllerBase + public sealed class AssetContentController : ApiController { private readonly IAssetStore assetStorage; private readonly IAssetRepository assetRepository; @@ -101,8 +101,10 @@ namespace Squidex.Controllers.Api.Assets } } } - - await assetStorage.DownloadAsync(assetId, asset.FileVersion, null, bodyStream); + else + { + await assetStorage.DownloadAsync(assetId, asset.FileVersion, null, bodyStream); + } }); } diff --git a/src/Squidex/Controllers/Api/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs similarity index 94% rename from src/Squidex/Controllers/Api/Assets/AssetsController.cs rename to src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 54c0ae939..fe83ccc82 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Assets.Models; +using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Write.Assets; @@ -26,7 +26,7 @@ using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Assets +namespace Squidex.Areas.Api.Controllers.Assets { /// /// Uploads and retrieves assets. @@ -35,7 +35,7 @@ namespace Squidex.Controllers.Api.Assets [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Assets))] - public sealed class AssetsController : ControllerBase + public sealed class AssetsController : ApiController { private readonly IAssetRepository assetRepository; private readonly IAssetStatsRepository assetStatsRepository; @@ -102,8 +102,8 @@ namespace Squidex.Controllers.Api.Assets } } - var taskForItems = assetRepository.QueryAsync(AppId, mimeTypeList, idsList, query, take, skip); - var taskForCount = assetRepository.CountAsync(AppId, mimeTypeList, idsList, query); + var taskForItems = assetRepository.QueryAsync(App.Id, mimeTypeList, idsList, query, take, skip); + var taskForCount = assetRepository.CountAsync(App.Id, mimeTypeList, idsList, query); await Task.WhenAll(taskForItems, taskForCount); @@ -164,7 +164,7 @@ namespace Squidex.Controllers.Api.Assets [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetCreatedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] - public async Task PostAsset(string app, List file) + public async Task PostAsset(string app, [SwaggerIgnore] List file) { var assetFile = await CheckAssetFileAsync(file); @@ -188,13 +188,16 @@ namespace Squidex.Controllers.Api.Assets /// 404 => Asset or app not found. /// 400 => Asset exceeds the maximum size. /// + /// + /// Use multipart request to upload an asset. + /// [MustBeAppEditor] [HttpPut] [Route("apps/{app}/assets/{id}/content/")] [ProducesResponseType(typeof(AssetReplacedDto), 201)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiCosts(1)] - public async Task PutAssetContent(string app, Guid id, List file) + public async Task PutAssetContent(string app, Guid id, [SwaggerIgnore] List file) { var assetFile = await CheckAssetFileAsync(file); diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Assets/Models/AssetCreatedDto.cs rename to src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs index d4ea54220..3265382b4 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs @@ -11,7 +11,7 @@ using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Write.Assets.Commands; using Squidex.Infrastructure.CQRS.Commands; -namespace Squidex.Controllers.Api.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models { public sealed class AssetCreatedDto { diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs rename to src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index ae3b560de..e5ac24f94 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -11,7 +11,7 @@ using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models { public sealed class AssetDto { diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetReplacedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Assets/Models/AssetReplacedDto.cs rename to src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs index ec5e438b2..a324b9ceb 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetReplacedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Write.Assets; using Squidex.Domain.Apps.Write.Assets.Commands; -namespace Squidex.Controllers.Api.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models { public sealed class AssetReplacedDto { diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs rename to src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs index 53b84ce34..da77022d9 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetUpdateDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models { public sealed class AssetUpdateDto { diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs rename to src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs index 0811b3f58..d00a2fc31 100644 --- a/src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Assets.Models +namespace Squidex.Areas.Api.Controllers.Assets.Models { public sealed class AssetsDto { diff --git a/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs similarity index 75% rename from src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs rename to src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs index b8a2899ab..be8fb0e6e 100644 --- a/src/Squidex/Controllers/ContentApi/ContentSwaggerController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentSwaggerController.cs @@ -9,25 +9,26 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.ContentApi.Generator; -using Squidex.Domain.Apps.Read.Schemas.Repositories; +using Squidex.Areas.Api.Controllers.Contents.Generator; +using Squidex.Domain.Apps.Read; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers.ContentApi +namespace Squidex.Areas.Api.Controllers.Contents { [ApiExceptionFilter] [AppApi] [SwaggerIgnore] - public sealed class ContentSwaggerController : ControllerBase + public sealed class ContentSwaggerController : ApiController { - private readonly ISchemaRepository schemaRepository; + private readonly IAppProvider appProvider; private readonly SchemasSwaggerGenerator schemasSwaggerGenerator; - public ContentSwaggerController(ICommandBus commandBus, ISchemaRepository schemaRepository, SchemasSwaggerGenerator schemasSwaggerGenerator) + public ContentSwaggerController(ICommandBus commandBus, IAppProvider appProvider, SchemasSwaggerGenerator schemasSwaggerGenerator) : base(commandBus) { - this.schemaRepository = schemaRepository; + this.appProvider = appProvider; + this.schemasSwaggerGenerator = schemasSwaggerGenerator; } @@ -46,7 +47,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(0)] public async Task GetSwagger(string app) { - var schemas = await schemaRepository.QueryAllAsync(App.Id); + var schemas = await appProvider.GetSchemasAsync(AppName); var swaggerDocument = await schemasSwaggerGenerator.Generate(App, schemas); diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs similarity index 98% rename from src/Squidex/Controllers/ContentApi/ContentsController.cs rename to src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs index 763a7c6fb..8ad56d2bc 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.ContentApi.Models; +using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Read.Contents; @@ -24,13 +24,13 @@ using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.ContentApi +namespace Squidex.Areas.Api.Controllers.Contents { [ApiAuthorize] [ApiExceptionFilter] [AppApi] [SwaggerIgnore] - public sealed class ContentsController : ControllerBase + public sealed class ContentsController : ApiController { private readonly IContentQueryService contentQuery; private readonly IContentVersionLoader contentVersionLoader; diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs similarity index 99% rename from src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs rename to src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs index 82840cb94..88d7500f9 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; using Squidex.Shared.Identity; -namespace Squidex.Controllers.ContentApi.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator { public sealed class SchemaSwaggerGenerator { diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs similarity index 91% rename from src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs rename to src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs index 73db22b1b..82295c3a5 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Generator/SchemasSwaggerGenerator.cs @@ -21,7 +21,7 @@ using Squidex.Domain.Apps.Read.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; -namespace Squidex.Controllers.ContentApi.Generator +namespace Squidex.Areas.Api.Controllers.Contents.Generator { public sealed class SchemasSwaggerGenerator { @@ -30,7 +30,6 @@ namespace Squidex.Controllers.ContentApi.Generator private readonly MyUrlsOptions urlOptions; private SwaggerJsonSchemaGenerator schemaGenerator; private JsonSchemaResolver schemaResolver; - private SwaggerGenerator swaggerGenerator; private SwaggerDocument document; public SchemasSwaggerGenerator(IHttpContextAccessor context, SwaggerSettings settings, IOptions urlOptions) @@ -47,8 +46,6 @@ namespace Squidex.Controllers.ContentApi.Generator schemaGenerator = new SwaggerJsonSchemaGenerator(settings); schemaResolver = new SwaggerSchemaResolver(document, settings); - swaggerGenerator = new SwaggerGenerator(schemaGenerator, settings, schemaResolver); - GenerateSchemasOperations(schemas, app); await GenerateDefaultErrorsAsync(); @@ -62,7 +59,7 @@ namespace Squidex.Controllers.ContentApi.Generator foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.SchemaDef)) { - new SchemaSwaggerGenerator(document, appBasePath, schema, AppendSchema, app.PartitionResolver).GenerateSchemaOperations(); + new SchemaSwaggerGenerator(document, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations(); } } diff --git a/src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/AssetsDto.cs similarity index 92% rename from src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs rename to src/Squidex/Areas/Api/Controllers/Content/Models/AssetsDto.cs index 62c345510..7888ba044 100644 --- a/src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/AssetsDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.ContentApi.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models { public sealed class AssetsDto { diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs similarity index 97% rename from src/Squidex/Controllers/ContentApi/Models/ContentDto.cs rename to src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index 3d95b00af..90daa7124 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -14,7 +14,7 @@ using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; -namespace Squidex.Controllers.ContentApi.Models +namespace Squidex.Areas.Api.Controllers.Contents.Models { public sealed class ContentDto { diff --git a/src/Squidex/Controllers/Api/Docs/DocsController.cs b/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs similarity index 73% rename from src/Squidex/Controllers/Api/Docs/DocsController.cs rename to src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs index 0a37fbb9d..a6f892596 100644 --- a/src/Squidex/Controllers/Api/Docs/DocsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs @@ -8,13 +8,19 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Docs +namespace Squidex.Areas.Api.Controllers.Docs { [SwaggerIgnore] - public sealed class DocsController : Controller + public sealed class DocsController : ApiController { + public DocsController(ICommandBus commandBus) + : base(commandBus) + { + } + [HttpGet] [Route("docs/")] [ApiCosts(0)] diff --git a/src/Squidex/Controllers/DocsVM.cs b/src/Squidex/Areas/Api/Controllers/DocsVM.cs similarity index 91% rename from src/Squidex/Controllers/DocsVM.cs rename to src/Squidex/Areas/Api/Controllers/DocsVM.cs index f71fd1b58..d51012baf 100644 --- a/src/Squidex/Controllers/DocsVM.cs +++ b/src/Squidex/Areas/Api/Controllers/DocsVM.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers +namespace Squidex.Areas.Api.Controllers { public sealed class DocsVM { diff --git a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs similarity index 58% rename from src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs rename to src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index 4d2228cf7..6a042222e 100644 --- a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -6,33 +6,32 @@ // All rights reserved. // ========================================================================== +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.EventConsumers.Models; -using Squidex.Infrastructure.Actors; -using Squidex.Infrastructure.CQRS.Events; +using Squidex.Areas.Api.Controllers.EventConsumers.Models; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Events.Actors.Messages; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.EventConsumers +namespace Squidex.Areas.Api.Controllers.EventConsumers { [ApiAuthorize] [ApiExceptionFilter] [MustBeAdministrator] [SwaggerIgnore] - public sealed class EventConsumersController : Controller + public sealed class EventConsumersController : ApiController { - private readonly IEventConsumerInfoRepository eventConsumerRepository; - private readonly IActors actors; + private readonly IPubSub pubSub; - public EventConsumersController(IEventConsumerInfoRepository eventConsumerRepository, IActors actors) + public EventConsumersController(ICommandBus commandBus, IPubSub pubSub) + : base(commandBus) { - this.eventConsumerRepository = eventConsumerRepository; - - this.actors = actors; + this.pubSub = pubSub; } [HttpGet] @@ -40,9 +39,9 @@ namespace Squidex.Controllers.Api.EventConsumers [ApiCosts(0)] public async Task GetEventConsumers() { - var entities = await eventConsumerRepository.QueryAsync(); + var entities = await pubSub.RequestAsync(new GetStatesRequest(), TimeSpan.FromSeconds(2), true); - var models = entities.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList(); + var models = entities.States.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList(); return Ok(models); } @@ -52,9 +51,7 @@ namespace Squidex.Controllers.Api.EventConsumers [ApiCosts(0)] public IActionResult Start(string name) { - var actor = actors.Get(name); - - actor?.Tell(new StartConsumerMessage()); + pubSub.Publish(new StartConsumerMessage { ConsumerName = name }, true); return NoContent(); } @@ -64,9 +61,7 @@ namespace Squidex.Controllers.Api.EventConsumers [ApiCosts(0)] public IActionResult Stop(string name) { - var actor = actors.Get(name); - - actor?.Tell(new StopConsumerMessage()); + pubSub.Publish(new StopConsumerMessage { ConsumerName = name }, true); return NoContent(); } @@ -76,9 +71,7 @@ namespace Squidex.Controllers.Api.EventConsumers [ApiCosts(0)] public IActionResult Reset(string name) { - var actor = actors.Get(name); - - actor?.Tell(new ResetConsumerMessage()); + pubSub.Publish(new ResetConsumerMessage { ConsumerName = name }, true); return NoContent(); } diff --git a/src/Squidex/Controllers/Api/EventConsumers/Models/EventConsumerDto.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs similarity index 90% rename from src/Squidex/Controllers/Api/EventConsumers/Models/EventConsumerDto.cs rename to src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs index 7fd83332f..f586c857a 100644 --- a/src/Squidex/Controllers/Api/EventConsumers/Models/EventConsumerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.EventConsumers.Models +namespace Squidex.Areas.Api.Controllers.EventConsumers.Models { public sealed class EventConsumerDto { diff --git a/src/Squidex/Controllers/Api/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs similarity index 92% rename from src/Squidex/Controllers/Api/History/HistoryController.cs rename to src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index b47b82b8a..e5a8d1775 100644 --- a/src/Squidex/Controllers/Api/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -10,13 +10,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.History.Models; +using Squidex.Areas.Api.Controllers.History.Models; using Squidex.Domain.Apps.Read.History.Repositories; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.History +namespace Squidex.Areas.Api.Controllers.History { /// /// Readonly API to get an event stream. @@ -26,7 +26,7 @@ namespace Squidex.Controllers.Api.History [AppApi] [MustBeAppEditor] [SwaggerTag(nameof(History))] - public sealed class HistoryController : ControllerBase + public sealed class HistoryController : ApiController { private readonly IHistoryEventRepository historyEventRepository; diff --git a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs b/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs rename to src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs index 92bfeb91e..48716de8a 100644 --- a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs @@ -10,7 +10,7 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; -namespace Squidex.Controllers.Api.History.Models +namespace Squidex.Areas.Api.Controllers.History.Models { public sealed class HistoryEventDto { diff --git a/src/Squidex/Controllers/JsonInheritanceConverter.cs b/src/Squidex/Areas/Api/Controllers/JsonInheritanceConverter.cs similarity index 98% rename from src/Squidex/Controllers/JsonInheritanceConverter.cs rename to src/Squidex/Areas/Api/Controllers/JsonInheritanceConverter.cs index 5076a3791..4f8387e52 100644 --- a/src/Squidex/Controllers/JsonInheritanceConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/JsonInheritanceConverter.cs @@ -16,7 +16,7 @@ using NJsonSchema.Annotations; #pragma warning disable SA1306 // Field names must begin with lower-case letter -namespace Squidex.Controllers +namespace Squidex.Areas.Api.Controllers { public sealed class JsonInheritanceConverter : JsonConverter { diff --git a/src/Squidex/Controllers/Api/LanguageDto.cs b/src/Squidex/Areas/Api/Controllers/LanguageDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/LanguageDto.cs rename to src/Squidex/Areas/Api/Controllers/LanguageDto.cs index 30f9b56a4..e56a22f16 100644 --- a/src/Squidex/Controllers/Api/LanguageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/LanguageDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api +namespace Squidex.Areas.Api.Controllers { public sealed class LanguageDto { diff --git a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs similarity index 83% rename from src/Squidex/Controllers/Api/Languages/LanguagesController.cs rename to src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index f87546cb1..a52140b0c 100644 --- a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -10,10 +10,11 @@ using System.Linq; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Languages +namespace Squidex.Areas.Api.Controllers.Languages { /// /// Readonly API to the supported langauges. @@ -21,8 +22,13 @@ namespace Squidex.Controllers.Api.Languages [ApiAuthorize] [ApiExceptionFilter] [SwaggerTag(nameof(Languages))] - public sealed class LanguagesController : Controller + public sealed class LanguagesController : ApiController { + public LanguagesController(ICommandBus commandBus) + : base(commandBus) + { + } + /// /// Get supported languages. /// diff --git a/src/Squidex/Controllers/Api/Ping/PingController.cs b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs similarity index 81% rename from src/Squidex/Controllers/Api/Ping/PingController.cs rename to src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index 730569034..2ae735915 100644 --- a/src/Squidex/Controllers/Api/Ping/PingController.cs +++ b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -8,9 +8,10 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Ping +namespace Squidex.Areas.Api.Controllers.Ping { /// /// Makes a ping request. @@ -20,8 +21,13 @@ namespace Squidex.Controllers.Api.Ping [AppApi] [MustBeAppReader] [SwaggerTag(nameof(Ping))] - public sealed class PingController : Controller + public sealed class PingController : ApiController { + public PingController(ICommandBus commandBus) + : base(commandBus) + { + } + /// /// Get ping status. /// diff --git a/src/Squidex/Controllers/Api/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs similarity index 95% rename from src/Squidex/Controllers/Api/Plans/AppPlansController.cs rename to src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index c69264f98..b362516fe 100644 --- a/src/Squidex/Controllers/Api/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -11,14 +11,14 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Plans.Models; +using Squidex.Areas.Api.Controllers.Plans.Models; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Plans +namespace Squidex.Areas.Api.Controllers.Plans { /// /// Manages and configures plans. @@ -27,7 +27,7 @@ namespace Squidex.Controllers.Api.Plans [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Plans))] - public sealed class AppPlansController : ControllerBase + public sealed class AppPlansController : ApiController { private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlanBillingManager appPlansBillingManager; diff --git a/src/Squidex/Controllers/Api/Plans/Models/AppPlansDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Plans/Models/AppPlansDto.cs rename to src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs index a44c341fe..0f797b446 100644 --- a/src/Squidex/Controllers/Api/Plans/Models/AppPlansDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/Models/AppPlansDto.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; -namespace Squidex.Controllers.Api.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models { public sealed class AppPlansDto { diff --git a/src/Squidex/Controllers/Api/Plans/Models/ChangePlanDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Plans/Models/ChangePlanDto.cs rename to src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs index 5daadd6d8..ac3155917 100644 --- a/src/Squidex/Controllers/Api/Plans/Models/ChangePlanDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/Models/ChangePlanDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models { public sealed class ChangePlanDto { diff --git a/src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs similarity index 90% rename from src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs rename to src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs index afb56d754..2c034d25b 100644 --- a/src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanChangedDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models { public sealed class PlanChangedDto { diff --git a/src/Squidex/Controllers/Api/Plans/Models/PlanDto.cs b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Plans/Models/PlanDto.cs rename to src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs index 0c45639ea..d7526d1bc 100644 --- a/src/Squidex/Controllers/Api/Plans/Models/PlanDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/Models/PlanDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Plans.Models +namespace Squidex.Areas.Api.Controllers.Plans.Models { public sealed class PlanDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/WebhookActionDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/WebhookActionDto.cs index d7f1c909e..c927ecf92 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Actions/WebhookActionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/WebhookActionDto.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Rules.Models.Actions +namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions { [JsonSchema("Webhook")] public sealed class WebhookActionDto : RuleActionDto diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs similarity index 89% rename from src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs index fe28cd931..09f12bdf6 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleActionDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleActionDtoFactory.cs @@ -6,12 +6,12 @@ // All rights reserved. // ========================================================================== -using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Areas.Api.Controllers.Rules.Models.Actions; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Rules.Models.Converters +namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters { public sealed class RuleActionDtoFactory : IRuleActionVisitor { diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs similarity index 87% rename from src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs index 339fabf2c..3cf4b64ed 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleConverter.cs @@ -11,7 +11,7 @@ using Squidex.Domain.Apps.Read.Rules; using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Rules.Models.Converters +namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters { public static class RuleConverter { @@ -20,16 +20,16 @@ namespace Squidex.Controllers.Api.Rules.Models.Converters var dto = new RuleDto(); SimpleMapper.Map(entity, dto); - SimpleMapper.Map(entity.Rule, dto); + SimpleMapper.Map(entity.RuleDef, dto); - if (entity.Rule.Trigger != null) + if (entity.RuleDef.Trigger != null) { - dto.Trigger = RuleTriggerDtoFactory.Create(entity.Rule.Trigger); + dto.Trigger = RuleTriggerDtoFactory.Create(entity.RuleDef.Trigger); } - if (entity.Rule.Action != null) + if (entity.RuleDef.Action != null) { - dto.Action = RuleActionDtoFactory.Create(entity.Rule.Action); + dto.Action = RuleActionDtoFactory.Create(entity.RuleDef.Action); } return dto; diff --git a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs similarity index 90% rename from src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index a9d38a6bf..b0bd6de67 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -7,12 +7,12 @@ // ========================================================================== using System.Linq; -using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Areas.Api.Controllers.Rules.Models.Triggers; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Rules.Models.Converters +namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters { public sealed class RuleTriggerDtoFactory : IRuleTriggerVisitor { diff --git a/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs index 060ff6461..4887c2761 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/CreateRuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/CreateRuleDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class CreateRuleDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs similarity index 85% rename from src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs index 2b95ed5b6..67ad0a53c 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleActionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionDto.cs @@ -8,10 +8,10 @@ using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Rules.Models.Actions; +using Squidex.Areas.Api.Controllers.Rules.Models.Actions; using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { [JsonConverter(typeof(JsonInheritanceConverter), "actionType")] [KnownType(typeof(WebhookActionDto))] diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index d6c3ade48..05c0afadd 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -11,7 +11,7 @@ using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class RuleDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs index 5513d5a25..1a09e88be 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventDto.cs @@ -12,7 +12,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Read.Rules; -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class RuleEventDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs index f0cda8a38..eec9ef2e7 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleEventsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleEventsDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class RuleEventsDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs similarity index 85% rename from src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index f65ea3f46..671dd25e3 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/RuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -8,10 +8,10 @@ using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Rules.Models.Triggers; +using Squidex.Areas.Api.Controllers.Rules.Models.Triggers; using Squidex.Domain.Apps.Core.Rules; -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { [JsonConverter(typeof(JsonInheritanceConverter), "triggerType")] [KnownType(typeof(ContentChangedTriggerDto))] diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerDto.cs similarity index 87% rename from src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerDto.cs index 05af3cf26..ab7fba80b 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerDto.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Linq; using NJsonSchema.Annotations; @@ -14,7 +15,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers { [JsonSchema("ContentChanged")] public sealed class ContentChangedTriggerDto : RuleTriggerDto @@ -29,7 +30,7 @@ namespace Squidex.Controllers.Api.Rules.Models.Triggers { return new ContentChangedTrigger { - Schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToList() + Schemas = Schemas.Select(x => SimpleMapper.Map(x, new ContentChangedTriggerSchema())).ToImmutableList() }; } } diff --git a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs index 29e0c71fd..cde7d7567 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/ContentChangedTriggerSchemaDto.cs @@ -8,7 +8,7 @@ using System; -namespace Squidex.Controllers.Api.Rules.Models.Triggers +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers { public sealed class ContentChangedTriggerSchemaDto { diff --git a/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs index bad710cc8..35bc2cc90 100644 --- a/src/Squidex/Controllers/Api/Rules/Models/UpdateRuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/UpdateRuleDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Rules.Models +namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class UpdateRuleDto { diff --git a/src/Squidex/Controllers/Api/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs similarity index 94% rename from src/Squidex/Controllers/Api/Rules/RulesController.cs rename to src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 100bb3633..f3efccc00 100644 --- a/src/Squidex/Controllers/Api/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -12,15 +12,16 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NodaTime; using NSwag.Annotations; -using Squidex.Controllers.Api.Rules.Models; -using Squidex.Controllers.Api.Rules.Models.Converters; +using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Areas.Api.Controllers.Rules.Models.Converters; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Rules.Repositories; using Squidex.Domain.Apps.Write.Rules.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Rules +namespace Squidex.Areas.Api.Controllers.Rules { /// /// Manages and retrieves information about schemas. @@ -30,17 +31,17 @@ namespace Squidex.Controllers.Api.Rules [AppApi] [SwaggerTag(nameof(Rules))] [MustBeAppDeveloper] - public sealed class RulesController : ControllerBase + public sealed class RulesController : ApiController { - private readonly IRuleRepository rulesRepository; + private readonly IAppProvider appProvider; private readonly IRuleEventRepository ruleEventsRepository; - public RulesController(ICommandBus commandBus, - IRuleRepository rulesRepository, + public RulesController(ICommandBus commandBus, IAppProvider appProvider, IRuleEventRepository ruleEventsRepository) : base(commandBus) { - this.rulesRepository = rulesRepository; + this.appProvider = appProvider; + this.ruleEventsRepository = ruleEventsRepository; } @@ -58,7 +59,7 @@ namespace Squidex.Controllers.Api.Rules [ApiCosts(1)] public async Task GetRules(string app) { - var rules = await rulesRepository.QueryByAppAsync(App.Id); + var rules = await appProvider.GetRulesAsync(AppName); var response = rules.Select(r => r.ToModel()); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs index 5b8672050..8e7f9d1be 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/AddFieldDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class AddFieldDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureScriptsDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureScriptsDto.cs index 0fe895a3e..cf13fcd66 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureScriptsDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class ConfigureScriptsDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index 5b803f707..33581dd9f 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -7,11 +7,11 @@ // ========================================================================== using System.Linq; -using Squidex.Controllers.Api.Schemas.Models.Fields; +using Squidex.Areas.Api.Controllers.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Converters +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters { public class FieldPropertiesDtoFactory : IFieldPropertiesVisitor { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs similarity index 97% rename from src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs index 9bd06f0de..3183569b1 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/SchemaConverter.cs @@ -11,7 +11,7 @@ using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Write.Schemas.Commands; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Converters +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters { public static class SchemaConverter { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs index b4da5cba2..3f3bfaec8 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class CreateSchemaDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaFieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaFieldDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaFieldDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaFieldDto.cs index 7a043aeb5..88c277b5b 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaFieldDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaFieldDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class CreateSchemaFieldDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/FieldDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs index 8e8c95f14..c6a2b0bf1 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class FieldDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index 31e33fbad..5540ebc26 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -9,10 +9,10 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Newtonsoft.Json; -using Squidex.Controllers.Api.Schemas.Models.Fields; +using Squidex.Areas.Api.Controllers.Schemas.Models.Fields; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { [JsonConverter(typeof(JsonInheritanceConverter), "fieldType")] [KnownType(typeof(AssetsFieldPropertiesDto))] diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index b2e7a3d50..c9d91f8a6 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Assets")] public sealed class AssetsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 8516d4b92..9f21f7178 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Boolean")] public sealed class BooleanFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs similarity index 96% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs index 4cdc78c49..f07dc09f1 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/DateTimeFieldPropertiesDto.cs @@ -13,7 +13,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("DateTime")] public sealed class DateTimeFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs index 76e1a1ce0..f684d6868 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/GeolocationFieldPropertiesDto.cs @@ -12,7 +12,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Geolocation")] public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs index 68f9fe290..fb3882223 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/JsonFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/JsonFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Json")] public sealed class JsonFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs similarity index 87% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index ff5b8fc14..d938c71f9 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -6,13 +6,14 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Number")] public sealed class NumberFieldPropertiesDto : FieldPropertiesDto @@ -47,6 +48,11 @@ namespace Squidex.Controllers.Api.Schemas.Models.Fields { var result = SimpleMapper.Map(this, new NumberFieldProperties()); + if (AllowedValues != null) + { + result.AllowedValues = ImmutableList.Create(AllowedValues); + } + return result; } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index e35541086..d1c85eda3 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -11,7 +11,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("References")] public sealed class ReferencesFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs similarity index 88% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index ebf41e4e7..25abd5b77 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -6,13 +6,14 @@ // All rights reserved. // ========================================================================== +using System.Collections.Immutable; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("String")] public sealed class StringFieldPropertiesDto : FieldPropertiesDto @@ -57,6 +58,11 @@ namespace Squidex.Controllers.Api.Schemas.Models.Fields { var result = SimpleMapper.Map(this, new StringFieldProperties()); + if (AllowedValues != null) + { + result.AllowedValues = ImmutableList.Create(AllowedValues); + } + return result; } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs similarity index 94% rename from src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs index 0769967ad..e50b40d57 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Fields/TagsFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/TagsFieldPropertiesDto.cs @@ -10,7 +10,7 @@ using NJsonSchema.Annotations; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Reflection; -namespace Squidex.Controllers.Api.Schemas.Models.Fields +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields { [JsonSchema("Tags")] public sealed class TagsFieldPropertiesDto : FieldPropertiesDto diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs index ae7cd21be..ff2062aab 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/ReorderFieldsDto.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class ReorderFieldsDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs similarity index 98% rename from src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs index 34e06a360..5fa61cc36 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs @@ -12,7 +12,7 @@ using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class SchemaDetailsDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs similarity index 97% rename from src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 1d6cf78e2..eb2031157 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -11,7 +11,7 @@ using System.ComponentModel.DataAnnotations; using NodaTime; using Squidex.Infrastructure; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class SchemaDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Schemas/Models/SchemaPropertiesDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs index 822c6f12b..d6fc03200 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class SchemaPropertiesDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/UpdateFieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Schemas/Models/UpdateFieldDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs index 6ec10e43c..082ecdaa2 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/UpdateFieldDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateFieldDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class UpdateFieldDto { diff --git a/src/Squidex/Controllers/Api/Schemas/Models/UpdateSchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Schemas/Models/UpdateSchemaDto.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs index 6c3ea84ed..e8a91646a 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/UpdateSchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Schemas.Models +namespace Squidex.Areas.Api.Controllers.Schemas.Models { public sealed class UpdateSchemaDto { diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs similarity index 98% rename from src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index c4ec2e529..fe08c3f8a 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -9,12 +9,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.Schemas.Models; +using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Domain.Apps.Write.Schemas.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Schemas +namespace Squidex.Areas.Api.Controllers.Schemas { /// /// Manages and retrieves information about schemas. @@ -24,7 +24,7 @@ namespace Squidex.Controllers.Api.Schemas [AppApi] [MustBeAppDeveloper] [SwaggerTag(nameof(Schemas))] - public sealed class SchemaFieldsController : ControllerBase + public sealed class SchemaFieldsController : ApiController { public SchemaFieldsController(ICommandBus commandBus) : base(commandBus) diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs similarity index 92% rename from src/Squidex/Controllers/Api/Schemas/SchemasController.cs rename to src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index b31a203db..65da638f2 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -12,17 +12,17 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; -using Squidex.Controllers.Api.Schemas.Models; -using Squidex.Controllers.Api.Schemas.Models.Converters; +using Squidex.Areas.Api.Controllers.Schemas.Models; +using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Repositories; using Squidex.Domain.Apps.Write.Schemas.Commands; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Schemas +namespace Squidex.Areas.Api.Controllers.Schemas { /// /// Manages and retrieves information about schemas. @@ -31,14 +31,14 @@ namespace Squidex.Controllers.Api.Schemas [ApiExceptionFilter] [AppApi] [SwaggerTag(nameof(Schemas))] - public sealed class SchemasController : ControllerBase + public sealed class SchemasController : ApiController { - private readonly ISchemaRepository schemaRepository; + private readonly IAppProvider appProvider; - public SchemasController(ICommandBus commandBus, ISchemaRepository schemaRepository) + public SchemasController(ICommandBus commandBus, IAppProvider appProvider) : base(commandBus) { - this.schemaRepository = schemaRepository; + this.appProvider = appProvider; } /// @@ -56,7 +56,7 @@ namespace Squidex.Controllers.Api.Schemas [ApiCosts(0)] public async Task GetSchemas(string app) { - var schemas = await schemaRepository.QueryAllAsync(AppId); + var schemas = await appProvider.GetSchemasAsync(AppName); var response = schemas.Select(s => s.ToModel()).ToList(); @@ -83,11 +83,11 @@ namespace Squidex.Controllers.Api.Schemas if (Guid.TryParse(name, out var id)) { - entity = await schemaRepository.FindSchemaAsync(id); + entity = await appProvider.GetSchemaAsync(AppName, id); } else { - entity = await schemaRepository.FindSchemaAsync(AppId, name); + entity = await appProvider.GetSchemaAsync(AppName, name); } if (entity == null || entity.IsDeleted) diff --git a/src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs rename to src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs index 99ff2038d..40f08251c 100644 --- a/src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsageDto.cs @@ -8,7 +8,7 @@ using System; -namespace Squidex.Controllers.Api.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models { public sealed class CallsUsageDto { diff --git a/src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs rename to src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs index 19d797079..3870c0572 100644 --- a/src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentCallsDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models { public sealed class CurrentCallsDto { diff --git a/src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs rename to src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs index 30d59396f..79b6550d8 100644 --- a/src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/Models/CurrentStorageDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models { public sealed class CurrentStorageDto { diff --git a/src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs b/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs rename to src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs index a03eef402..205ebcbb4 100644 --- a/src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs @@ -8,7 +8,7 @@ using System; -namespace Squidex.Controllers.Api.Statistics.Models +namespace Squidex.Areas.Api.Controllers.Statistics.Models { public sealed class StorageUsageDto { diff --git a/src/Squidex/Controllers/Api/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs similarity index 97% rename from src/Squidex/Controllers/Api/Statistics/UsagesController.cs rename to src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index a3b0a1f16..2be660d10 100644 --- a/src/Squidex/Controllers/Api/Statistics/UsagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -11,14 +11,14 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.Statistics.Models; +using Squidex.Areas.Api.Controllers.Statistics.Models; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.Statistics +namespace Squidex.Areas.Api.Controllers.Statistics { /// /// Retrieves usage information for apps. @@ -28,7 +28,7 @@ namespace Squidex.Controllers.Api.Statistics [AppApi] [MustBeAppEditor] [SwaggerTag(nameof(Statistics))] - public sealed class UsagesController : ControllerBase + public sealed class UsagesController : ApiController { private readonly IUsageTracker usageTracker; private readonly IAppPlansProvider appPlanProvider; diff --git a/src/Squidex/Controllers/Api/UI/Models/UIRegexSuggestionDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs similarity index 93% rename from src/Squidex/Controllers/Api/UI/Models/UIRegexSuggestionDto.cs rename to src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs index cd47f6fd1..363c86758 100644 --- a/src/Squidex/Controllers/Api/UI/Models/UIRegexSuggestionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.UI.Models +namespace Squidex.Areas.Api.Controllers.UI.Models { public sealed class UIRegexSuggestionDto { diff --git a/src/Squidex/Controllers/Api/UI/Models/UISettingsDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/UI/Models/UISettingsDto.cs rename to src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index 73df39d84..6c051df7f 100644 --- a/src/Squidex/Controllers/Api/UI/Models/UISettingsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.UI.Models +namespace Squidex.Areas.Api.Controllers.UI.Models { public sealed class UISettingsDto { diff --git a/src/Squidex/Controllers/Api/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs similarity index 83% rename from src/Squidex/Controllers/Api/UI/UIController.cs rename to src/Squidex/Areas/Api/Controllers/UI/UIController.cs index c94b4450a..ce0ce7018 100644 --- a/src/Squidex/Controllers/Api/UI/UIController.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -11,22 +11,24 @@ using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Config; -using Squidex.Controllers.Api.UI.Models; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Pipeline; -namespace Squidex.Controllers.Api.UI +namespace Squidex.Areas.Api.Controllers.UI { /// /// Manages ui settings and configs. /// [ApiExceptionFilter] [SwaggerTag(nameof(UI))] - public sealed class UIController : Controller + public sealed class UIController : ApiController { private readonly MyUIOptions uiOptions; - public UIController(IOptions uiOptions) + public UIController(ICommandBus commandBus, IOptions uiOptions) + : base(commandBus) { this.uiOptions = uiOptions.Value; } diff --git a/src/Squidex/Controllers/Api/Users/Assets/Avatar.png b/src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png similarity index 100% rename from src/Squidex/Controllers/Api/Users/Assets/Avatar.png rename to src/Squidex/Areas/Api/Controllers/Users/Assets/Avatar.png diff --git a/src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs rename to src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs index 77a4167c4..bdeab5881 100644 --- a/src/Squidex/Controllers/Api/Users/Models/CreateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class CreateUserDto { diff --git a/src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs rename to src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs index ed8569ad2..fada1e7be 100644 --- a/src/Squidex/Controllers/Api/Users/Models/UpdateUserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UpdateUserDto { diff --git a/src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs similarity index 91% rename from src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs rename to src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs index 3e84a714c..4f7ad78f1 100644 --- a/src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UserCreatedDto { diff --git a/src/Squidex/Controllers/Api/Users/Models/UserDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs similarity index 95% rename from src/Squidex/Controllers/Api/Users/Models/UserDto.cs rename to src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index cc2778101..b674c0ab8 100644 --- a/src/Squidex/Controllers/Api/Users/Models/UserDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.Api.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UserDto { diff --git a/src/Squidex/Controllers/Api/Users/Models/UsersDto.cs b/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs similarity index 92% rename from src/Squidex/Controllers/Api/Users/Models/UsersDto.cs rename to src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs index 919150480..01e8ed900 100644 --- a/src/Squidex/Controllers/Api/Users/Models/UsersDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Users.Models +namespace Squidex.Areas.Api.Controllers.Users.Models { public sealed class UsersDto { diff --git a/src/Squidex/Controllers/Api/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs similarity index 91% rename from src/Squidex/Controllers/Api/Users/UserManagementController.cs rename to src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index 59b647753..5593b3b2e 100644 --- a/src/Squidex/Controllers/Api/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -12,26 +12,28 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.Users.Models; +using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Pipeline; using Squidex.Shared.Users; -namespace Squidex.Controllers.Api.Users +namespace Squidex.Areas.Api.Controllers.Users { [ApiAuthorize] [ApiExceptionFilter] [MustBeAdministrator] [SwaggerIgnore] - public sealed class UserManagementController : Controller + public sealed class UserManagementController : ApiController { private readonly UserManager userManager; private readonly IUserFactory userFactory; - public UserManagementController(UserManager userManager, IUserFactory userFactory) + public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory) + : base(commandBus) { this.userManager = userManager; this.userFactory = userFactory; diff --git a/src/Squidex/Controllers/Api/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs similarity index 92% rename from src/Squidex/Controllers/Api/Users/UsersController.cs rename to src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 5ec95aedd..9b8582e81 100644 --- a/src/Squidex/Controllers/Api/Users/UsersController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -14,20 +14,21 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -using Squidex.Controllers.Api.Users.Models; +using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; using Squidex.Shared.Users; -namespace Squidex.Controllers.Api.Users +namespace Squidex.Areas.Api.Controllers.Users { /// /// Readonly API to retrieve information about squidex users. /// [ApiExceptionFilter] [SwaggerTag(nameof(Users))] - public sealed class UsersController : Controller + public sealed class UsersController : ApiController { private static readonly byte[] AvatarBytes; private readonly UserManager userManager; @@ -37,7 +38,7 @@ namespace Squidex.Controllers.Api.Users { var assembly = typeof(UsersController).GetTypeInfo().Assembly; - using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Controllers.Api.Users.Assets.Avatar.png")) + using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) { AvatarBytes = new byte[avatarStream.Length]; @@ -45,7 +46,8 @@ namespace Squidex.Controllers.Api.Users } } - public UsersController(UserManager userManager, IUserPictureStore userPictureStore) + public UsersController(ICommandBus commandBus, UserManager userManager, IUserPictureStore userPictureStore) + : base(commandBus) { this.userManager = userManager; this.userPictureStore = userPictureStore; diff --git a/src/Squidex/Areas/Api/Startup.cs b/src/Squidex/Areas/Api/Startup.cs new file mode 100644 index 000000000..70f83f883 --- /dev/null +++ b/src/Squidex/Areas/Api/Startup.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Startup.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Squidex.Areas.Api.Config.Swagger; +using Squidex.Config; + +namespace Squidex.Areas.Api +{ + public static class Startup + { + public static void ConfigureApi(this IApplicationBuilder app) + { + app.Map(Constants.ApiPrefix, appApi => + { + appApi.UseMySwagger(); + appApi.UseMvc(); + }); + } + } +} diff --git a/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml b/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml new file mode 100644 index 000000000..889bad8fc --- /dev/null +++ b/src/Squidex/Areas/Api/Views/Shared/Docs.cshtml @@ -0,0 +1,24 @@ +@model Squidex.Areas.Api.Controllers.DocsVM + +@{ + Layout = null; +} + + + + + API Docs + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Squidex/Pipeline/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs similarity index 98% rename from src/Squidex/Pipeline/WebpackMiddleware.cs rename to src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs index 71f14245a..8a4686ecf 100644 --- a/src/Squidex/Pipeline/WebpackMiddleware.cs +++ b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -11,7 +11,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Squidex.Pipeline +namespace Squidex.Areas.Frontend.Middlewares { public sealed class WebpackMiddleware { diff --git a/src/Squidex/Areas/Frontend/Startup.cs b/src/Squidex/Areas/Frontend/Startup.cs new file mode 100644 index 000000000..2dc954cc7 --- /dev/null +++ b/src/Squidex/Areas/Frontend/Startup.cs @@ -0,0 +1,81 @@ +// ========================================================================== +// Startup.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Frontend.Middlewares; + +namespace Squidex.Areas.Frontend +{ + public static class Startup + { + public static void ConfigureFrontend(this IApplicationBuilder app) + { + var environment = app.ApplicationServices.GetRequiredService(); + + if (environment.IsDevelopment()) + { + app.UseMiddleware(); + } + + app.Use((context, next) => + { + if (context.Request.Path == "/client-callback-popup") + { + context.Request.Path = new PathString("/client-callback-popup.html"); + } + else if (context.Request.Path == "/client-callback-silent") + { + context.Request.Path = new PathString("/client-callback-silent.html"); + } + else if (!Path.HasExtension(context.Request.Path.Value)) + { + if (environment.IsDevelopment()) + { + context.Request.Path = new PathString("/index.html"); + } + else + { + context.Request.Path = new PathString("/build/index.html"); + } + } + + return next(); + }); + + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = context => + { + var response = context.Context.Response; + var responseHeaders = response.GetTypedHeaders(); + + if (!string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + MaxAge = TimeSpan.FromDays(60) + }; + } + else + { + responseHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true + }; + } + } + }); + } + } +} diff --git a/src/Squidex/Config/Identity/Cert/IdentityCert.pfx b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx similarity index 100% rename from src/Squidex/Config/Identity/Cert/IdentityCert.pfx rename to src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.pfx diff --git a/src/Squidex/Config/Identity/Cert/IdentityCert.snk b/src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk similarity index 100% rename from src/Squidex/Config/Identity/Cert/IdentityCert.snk rename to src/Squidex/Areas/IdentityServer/Config/Cert/IdentityCert.snk diff --git a/src/Squidex/Config/Identity/IdentityExtensions.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs similarity index 69% rename from src/Squidex/Config/Identity/IdentityExtensions.cs rename to src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs index 6bcee01ee..1cc9a4e66 100644 --- a/src/Squidex/Config/Identity/IdentityExtensions.cs +++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IdentityExtensions.cs +// IdentityServerExtensions.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -13,14 +13,15 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Squidex.Config; using Squidex.Domain.Users; using Squidex.Infrastructure.Log; using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Config.Identity +namespace Squidex.Areas.IdentityServer.Config { - public static class IdentityExtensions + public static class IdentityServerExtensions { public static IApplicationBuilder UseMyIdentityServer(this IApplicationBuilder app) { @@ -29,24 +30,24 @@ namespace Squidex.Config.Identity return app; } - public static IApplicationBuilder UseMyAdminRole(this IApplicationBuilder app) + public static IServiceProvider UseMyAdminRole(this IServiceProvider services) { - var roleManager = app.ApplicationServices.GetRequiredService>(); - var roleFactory = app.ApplicationServices.GetRequiredService(); + var roleManager = services.GetRequiredService>(); + var roleFactory = services.GetRequiredService(); roleManager.CreateAsync(roleFactory.Create(SquidexRoles.Administrator)).Wait(); - return app; + return services; } - public static IApplicationBuilder UseMyAdmin(this IApplicationBuilder app) + public static IServiceProvider UseMyAdmin(this IServiceProvider services) { - var options = app.ApplicationServices.GetService>().Value; + var options = services.GetService>().Value; - var userManager = app.ApplicationServices.GetService>(); - var userFactory = app.ApplicationServices.GetService(); + var userManager = services.GetService>(); + var userFactory = services.GetService(); - var log = app.ApplicationServices.GetService(); + var log = services.GetService(); if (options.IsAdminConfigured()) { @@ -73,7 +74,7 @@ namespace Squidex.Config.Identity }).Wait(); } - return app; + return services; } } } diff --git a/src/Squidex/Config/Identity/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs similarity index 73% rename from src/Squidex/Config/Identity/IdentityServerServices.cs rename to src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index 2416df114..95c689f14 100644 --- a/src/Squidex/Config/Identity/IdentityServerServices.cs +++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -12,23 +12,28 @@ using System.Security.Cryptography.X509Certificates; using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Stores; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +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.Shared.Users; -namespace Squidex.Config.Identity +namespace Squidex.Areas.IdentityServer.Config { public static class IdentityServerServices { - public static IServiceCollection AddMyIdentityServer(this IServiceCollection services) + public static void AddMyIdentityServer(this IServiceCollection services) { X509Certificate2 certificate; - var assembly = typeof(IdentityServices).GetTypeInfo().Assembly; + var assembly = typeof(IdentityServerServices).GetTypeInfo().Assembly; - using (var certStream = assembly.GetManifestResourceStream("Squidex.Config.Identity.Cert.IdentityCert.pfx")) + using (var certStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) { var certData = new byte[certStream.Length]; @@ -39,10 +44,20 @@ namespace Squidex.Config.Identity X509KeyStorageFlags.Exportable); } - services.AddSingleton( - GetApiResources()); - services.AddSingleton( - GetIdentityResources()); + services.AddSingleton>(s => + { + return new ConfigureOptions(options => + { + options.XmlRepository = s.GetRequiredService(); + }); + }); + + services.AddDataProtection().SetApplicationName("Squidex"); + services.AddSingleton(GetApiResources()); + services.AddSingleton(GetIdentityResources()); + + services.AddIdentity() + .AddDefaultTokenProviders(); services.AddSingleton, UserClaimsPrincipalFactoryWithEmail>(); services.AddSingleton(); services.AddIdentityServer(options => - { - options.UserInteraction.ErrorUrl = "/error/"; - }) + { + options.UserInteraction.ErrorUrl = "/error/"; + }) .AddAspNetIdentity() .AddInMemoryApiResources(GetApiResources()) .AddInMemoryIdentityResources(GetIdentityResources()) .AddSigningCredential(certificate); - - return services; - } - - public static IServiceCollection AddMyIdentity(this IServiceCollection services) - { - services.AddIdentity() - .AddDefaultTokenProviders(); - - return services; } private static IEnumerable GetApiResources() diff --git a/src/Squidex/Config/Identity/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs similarity index 66% rename from src/Squidex/Config/Identity/LazyClientStore.cs rename to src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 2fb3a33ca..7750dd4fb 100644 --- a/src/Squidex/Config/Identity/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -13,23 +13,24 @@ using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Options; +using Squidex.Config; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Read; using Squidex.Infrastructure; -namespace Squidex.Config.Identity +namespace Squidex.Areas.IdentityServer.Config { public class LazyClientStore : IClientStore { - private readonly IAppProvider appProvider; + private readonly IAppProvider appState; private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); - public LazyClientStore(IOptions urlsOptions, IAppProvider appProvider) + public LazyClientStore(IOptions urlsOptions, IAppProvider appState) { Guard.NotNull(urlsOptions, nameof(urlsOptions)); - Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(appState, nameof(appState)); - this.appProvider = appProvider; + this.appState = appState; CreateStaticClients(urlsOptions); } @@ -50,7 +51,7 @@ namespace Squidex.Config.Identity return null; } - var app = await appProvider.FindAppByNameAsync(token[0]); + var app = await appState.GetAppAsync(token[0]); var appClient = app?.Clients.GetOrDefault(token[1]); @@ -91,17 +92,17 @@ namespace Squidex.Config.Identity private static IEnumerable CreateStaticClients(MyUrlsOptions urlsOptions) { - var id = Constants.FrontendClient; + var frontendId = Constants.FrontendClient; yield return new Client { - ClientId = id, - ClientName = id, + ClientId = frontendId, + ClientName = frontendId, RedirectUris = new List { urlsOptions.BuildUrl("login;"), - urlsOptions.BuildUrl("identity-server/client-callback-silent/"), - urlsOptions.BuildUrl("identity-server/client-callback-popup/") + urlsOptions.BuildUrl("client-callback-silent", false), + urlsOptions.BuildUrl("client-callback-popup", false) }, PostLogoutRedirectUris = new List { @@ -120,6 +121,32 @@ namespace Squidex.Config.Identity }, RequireConsent = false }; + + var internalClient = Constants.InternalClientId; + + yield return new Client + { + ClientId = internalClient, + ClientName = internalClient, + ClientSecrets = new List { new Secret(Constants.InternalClientSecret) }, + RedirectUris = new List + { + urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-oidc", false), + urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-oidc", false) + }, + AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, + AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, + AllowedScopes = new List + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + Constants.ApiScope, + Constants.ProfileScope, + Constants.RoleScope + }, + RequireConsent = false + }; } } } diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs similarity index 94% rename from src/Squidex/Controllers/UI/Account/AccountController.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 55ff820f6..ed9bf1ac3 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; using Squidex.Config; -using Squidex.Config.Identity; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -27,10 +26,10 @@ using Squidex.Infrastructure.Tasks; using Squidex.Shared.Identity; using Squidex.Shared.Users; -namespace Squidex.Controllers.UI.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account { [SwaggerIgnore] - public sealed class AccountController : Controller + public sealed class AccountController : IdentityServerController { private readonly SignInManager signInManager; private readonly UserManager userManager; @@ -61,20 +60,6 @@ namespace Squidex.Controllers.UI.Account this.signInManager = signInManager; } - [HttpGet] - [Route("client-callback-silent/")] - public IActionResult ClientSilent() - { - return View(); - } - - [HttpGet] - [Route("client-callback-popup/")] - public IActionResult ClientPopup() - { - return View(); - } - [HttpGet] [Route("account/forbidden/")] public IActionResult Forbidden() @@ -173,8 +158,7 @@ namespace Squidex.Controllers.UI.Account { var allowPasswordAuth = identityOptions.Value.AllowPasswordAuth; - var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); - var externalProviders = externalSchemes.Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); + var externalProviders = await signInManager.GetExternalProvidersAsync(); var vm = new LoginVM { diff --git a/src/Squidex/Controllers/UI/Account/LoginModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs similarity index 90% rename from src/Squidex/Controllers/UI/Account/LoginModel.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs index ca70d9aca..3fbcecaba 100644 --- a/src/Squidex/Controllers/UI/Account/LoginModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginModel.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.UI.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account { public sealed class LoginModel { diff --git a/src/Squidex/Controllers/UI/Account/LoginVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs similarity index 92% rename from src/Squidex/Controllers/UI/Account/LoginVM.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs index 0b047d552..a36dc068c 100644 --- a/src/Squidex/Controllers/UI/Account/LoginVM.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Account/LoginVM.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; -namespace Squidex.Controllers.UI.Account +namespace Squidex.Areas.IdentityServer.Controllers.Account { public class LoginVM { diff --git a/src/Squidex/Controllers/UI/Error/ErrorController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs similarity index 81% rename from src/Squidex/Controllers/UI/Error/ErrorController.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs index 6726d7e72..d3055202b 100644 --- a/src/Squidex/Controllers/UI/Error/ErrorController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Error/ErrorController.cs @@ -9,10 +9,10 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; -namespace Squidex.Controllers.UI.Error +namespace Squidex.Areas.IdentityServer.Controllers.Error { [SwaggerIgnore] - public sealed class ErrorController : Controller + public sealed class ErrorController : IdentityServerController { [Route("error/")] public IActionResult Error() diff --git a/src/Squidex/Controllers/UI/Extensions.cs b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs similarity index 59% rename from src/Squidex/Controllers/UI/Extensions.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs index 1ba7f7e63..ae0f600ee 100644 --- a/src/Squidex/Controllers/UI/Extensions.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs @@ -6,24 +6,18 @@ // All rights reserved. // ========================================================================== +using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Identity; -using Squidex.Domain.Users; using Squidex.Shared.Users; -namespace Squidex.Controllers.UI +namespace Squidex.Areas.IdentityServer.Controllers { public static class Extensions { - public static Task UpdateAsync(this UserManager userManager, IUser user, string email, string displayName) - { - user.UpdateEmail(email); - user.SetDisplayName(displayName); - - return userManager.UpdateAsync(user); - } - public static async Task GetExternalLoginInfoWithDisplayNameAsync(this SignInManager signInManager, string expectedXsrf = null) { var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf); @@ -32,5 +26,15 @@ namespace Squidex.Controllers.UI return externalLogin; } + + public static async Task> GetExternalProvidersAsync(this SignInManager signInManager) + { + var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); + var externalProviders = + externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme) + .Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); + + return externalProviders; + } } } diff --git a/src/Squidex/Controllers/UI/ExternalProvider.cs b/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs similarity index 92% rename from src/Squidex/Controllers/UI/ExternalProvider.cs rename to src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs index 26574c5b1..26ec5ec9c 100644 --- a/src/Squidex/Controllers/UI/ExternalProvider.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/ExternalProvider.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.UI +namespace Squidex.Areas.IdentityServer.Controllers { public class ExternalProvider { diff --git a/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs b/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs new file mode 100644 index 000000000..ab74b043b --- /dev/null +++ b/src/Squidex/Areas/IdentityServer/Controllers/IdentityServerController.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Extensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Squidex.Areas.IdentityServer.Controllers +{ + [Area("IdentityServer")] + public abstract class IdentityServerController : Controller + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.HttpContext.Request.PathBase.StartsWithSegments("/identity-server")) + { + context.Result = new NotFoundResult(); + } + } + } +} diff --git a/src/Squidex/Controllers/UI/Profile/ChangePasswordModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs similarity index 93% rename from src/Squidex/Controllers/UI/Profile/ChangePasswordModel.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs index 669742723..6ec711857 100644 --- a/src/Squidex/Controllers/UI/Profile/ChangePasswordModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangePasswordModel.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { public class ChangePasswordModel { diff --git a/src/Squidex/Controllers/UI/Profile/ChangeProfileModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs similarity index 92% rename from src/Squidex/Controllers/UI/Profile/ChangeProfileModel.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs index 2969c0dfa..5d734bc94 100644 --- a/src/Squidex/Controllers/UI/Profile/ChangeProfileModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ChangeProfileModel.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { public class ChangeProfileModel { diff --git a/src/Squidex/Controllers/UI/Profile/ProfileController.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs similarity index 95% rename from src/Squidex/Controllers/UI/Profile/ProfileController.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index e09e105cd..dd08b4dfb 100644 --- a/src/Squidex/Controllers/UI/Profile/ProfileController.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -18,17 +18,17 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; -using Squidex.Config.Identity; +using Squidex.Config; using Squidex.Domain.Users; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Reflection; using Squidex.Shared.Users; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { [Authorize] [SwaggerIgnore] - public sealed class ProfileController : Controller + public sealed class ProfileController : IdentityServerController { private readonly SignInManager signInManager; private readonly UserManager userManager; @@ -180,8 +180,7 @@ namespace Squidex.Controllers.UI.Profile private async Task GetProfileVM(IUser user, ChangeProfileModel model = null, string errorMessage = null, string successMessage = null) { - var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync(); - var externalProviders = externalSchemes.Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList(); + var externalProviders = await signInManager.GetExternalProvidersAsync(); var result = new ProfileVM { diff --git a/src/Squidex/Controllers/UI/Profile/ProfileVM.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs similarity index 94% rename from src/Squidex/Controllers/UI/Profile/ProfileVM.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs index 730589a46..650814bf4 100644 --- a/src/Squidex/Controllers/UI/Profile/ProfileVM.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileVM.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using Squidex.Shared.Users; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { public sealed class ProfileVM { diff --git a/src/Squidex/Controllers/UI/Profile/RemoveLoginModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs similarity index 91% rename from src/Squidex/Controllers/UI/Profile/RemoveLoginModel.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs index 415608b18..6682bdb1e 100644 --- a/src/Squidex/Controllers/UI/Profile/RemoveLoginModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/RemoveLoginModel.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { public class RemoveLoginModel { diff --git a/src/Squidex/Controllers/UI/Profile/SetPasswordModel.cs b/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs similarity index 92% rename from src/Squidex/Controllers/UI/Profile/SetPasswordModel.cs rename to src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs index cb9ab2dbe..1f6f8310b 100644 --- a/src/Squidex/Controllers/UI/Profile/SetPasswordModel.cs +++ b/src/Squidex/Areas/IdentityServer/Controllers/Profile/SetPasswordModel.cs @@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Controllers.UI.Profile +namespace Squidex.Areas.IdentityServer.Controllers.Profile { public class SetPasswordModel { diff --git a/src/Squidex/Areas/IdentityServer/Startup.cs b/src/Squidex/Areas/IdentityServer/Startup.cs new file mode 100644 index 000000000..b43b8d1ef --- /dev/null +++ b/src/Squidex/Areas/IdentityServer/Startup.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Startup.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Areas.IdentityServer.Config; +using Squidex.Config; + +namespace Squidex.Areas.IdentityServer +{ + public static class Startup + { + public static void ConfigureIdentityServer(this IApplicationBuilder app) + { + app.ApplicationServices.UseMyAdminRole(); + app.ApplicationServices.UseMyAdmin(); + + var environment = app.ApplicationServices.GetRequiredService(); + + app.Map(Constants.IdentityServerPrefix, identityApp => + { + identityApp.UseMyIdentityServer(); + + if (environment.IsDevelopment()) + { + identityApp.UseDeveloperExceptionPage(); + } + else + { + identityApp.UseExceptionHandler("/error"); + } + + identityApp.UseStaticFiles(); + identityApp.UseMvc(); + }); + } + } +} diff --git a/src/Squidex/Views/Account/AccessDenied.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml similarity index 100% rename from src/Squidex/Views/Account/AccessDenied.cshtml rename to src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml diff --git a/src/Squidex/Views/Account/LockedOut.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml similarity index 100% rename from src/Squidex/Views/Account/LockedOut.cshtml rename to src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml diff --git a/src/Squidex/Views/Account/Login.cshtml b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml similarity index 96% rename from src/Squidex/Views/Account/Login.cshtml rename to src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml index d64812b2a..0e80cdc89 100644 --- a/src/Squidex/Views/Account/Login.cshtml +++ b/src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml @@ -1,4 +1,4 @@ -@model Squidex.Controllers.UI.Account.LoginVM +@model Squidex.Areas.IdentityServer.Controllers.Account.LoginVM @{ var type = Model.IsLogin ? "Login" : "Signup"; @@ -94,7 +94,7 @@ } -@if (!Model.HasPasswordAuth && Model.ExternalProviders.Count() == 1) +@if (!Model.HasPasswordAuth && Model.ExternalProviders.Count == 1) { - - - diff --git a/src/Squidex/Views/Account/ClientSilent.cshtml b/src/Squidex/Views/Account/ClientSilent.cshtml deleted file mode 100644 index 39f659cb3..000000000 --- a/src/Squidex/Views/Account/ClientSilent.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@{ - Layout = null; -} - - - - - - - - diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml deleted file mode 100644 index 15b0e282a..000000000 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ /dev/null @@ -1,40 +0,0 @@ -@model Squidex.Controllers.DocsVM - -@{ - Layout = null; -} - - - - - API Docs - - - - - - - - - - - \ No newline at end of file diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 47195309c..2abdbadc3 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebApp.cs +// WebStartup.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,40 +7,26 @@ // ========================================================================== using System; -using System.IO; -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Squidex.Config; +using Squidex.Areas.Api; +using Squidex.Areas.Frontend; +using Squidex.Areas.IdentityServer; +using Squidex.Areas.Portal; using Squidex.Config.Domain; -using Squidex.Config.Identity; -using Squidex.Config.Swagger; using Squidex.Config.Web; -using Squidex.Infrastructure.Log; - -#pragma warning disable RECS0002 // Convert anonymous method to method group namespace Squidex { - public class WebStartup : IStartup + public sealed class WebStartup : IStartup { private readonly IConfiguration configuration; - private readonly IHostingEnvironment environment; - private static readonly string[] IdentityServerPaths = - { - "/client-callback-popup", - "/client-callback-silent", - "/account", - "/error" - }; - public WebStartup(IConfiguration configuration, IHostingEnvironment environment) + public WebStartup(IConfiguration configuration) { this.configuration = configuration; - this.environment = environment; } public IServiceProvider ConfigureServices(IServiceCollection services) @@ -59,91 +45,11 @@ namespace Squidex app.UseMyForwardingRules(); app.UseMyTracking(); - MapAndUseIdentityServer(app); - MapAndUseApi(app); - MapAndUseFrontend(app); - - app.ApplicationServices.UseMyEventStore(); - } - - private void MapAndUseIdentityServer(IApplicationBuilder app) - { - app.Map(Constants.IdentityPrefix, identityApp => - { - if (environment.IsDevelopment()) - { - identityApp.UseDeveloperExceptionPage(); - } - else - { - identityApp.UseExceptionHandler("/error"); - } - - identityApp.UseMyAuthentication(); - identityApp.UseMyIdentityServer(); - identityApp.UseMyAdminRole(); - identityApp.UseMyAdmin(); - identityApp.UseStaticFiles(); - - identityApp.MapWhen(IsIdentityRequest, mvcApp => - { - mvcApp.UseMvc(); - }); - }); - } - - private void MapAndUseApi(IApplicationBuilder app) - { - app.Map(Constants.ApiPrefix, appApi => - { - if (environment.IsDevelopment()) - { - appApi.UseDeveloperExceptionPage(); - } + app.ConfigureApi(); + app.ConfigurePortal(); + app.ConfigureIdentityServer(); - appApi.UseMySwagger(); - - appApi.MapWhen(x => !IsIdentityRequest(x), mvcApp => - { - mvcApp.UseMvc(); - }); - }); - } - - private void MapAndUseFrontend(IApplicationBuilder app) - { - if (environment.IsDevelopment()) - { - app.UseWebpackProxy(); - - app.Use((context, next) => - { - if (!Path.HasExtension(context.Request.Path.Value)) - { - context.Request.Path = new PathString("/index.html"); - } - return next(); - }); - } - else - { - app.Use((context, next) => - { - if (!Path.HasExtension(context.Request.Path.Value)) - { - context.Request.Path = new PathString("/build/index.html"); - } - - return next(); - }); - } - - app.UseMyCachedStaticFiles(); - } - - private static bool IsIdentityRequest(HttpContext context) - { - return IdentityServerPaths.Any(p => context.Request.Path.StartsWithSegments(p)); + app.ConfigureFrontend(); } } } diff --git a/src/Squidex/app/features/administration/administration-area.component.html b/src/Squidex/app/features/administration/administration-area.component.html index e75dab4a4..35258c707 100644 --- a/src/Squidex/app/features/administration/administration-area.component.html +++ b/src/Squidex/app/features/administration/administration-area.component.html @@ -12,6 +12,11 @@ + diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 855d37bce..a5c70ebf4 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -24,7 +24,7 @@
- +
@@ -47,10 +47,10 @@
- +
- +
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 3569894ae..83ba37442 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -1,6 +1,6 @@ - +
@@ -44,7 +44,7 @@

- {{schema | sqxDisplayName:'properties.label':'name'}} + {{schema | sqxDisplayName:'properties.label':'name'}}

diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/src/Squidex/app/features/settings/settings-area.component.html index 36e78e5a5..7dd011dad 100644 --- a/src/Squidex/app/features/settings/settings-area.component.html +++ b/src/Squidex/app/features/settings/settings-area.component.html @@ -20,7 +20,7 @@ - - -