diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs index 4d788dd3a..2c52c8307 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrain.cs @@ -18,14 +18,12 @@ 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.Grains { public class AppStateGrain : StatefulObject { private readonly FieldRegistry fieldRegistry; - private readonly TaskFactory taskFactory = new TaskFactory(new LimitedConcurrencyLevelTaskScheduler(1)); private Exception exception; public AppStateGrain(FieldRegistry fieldRegistry) @@ -53,101 +51,77 @@ namespace Squidex.Domain.Apps.Read.State.Grains public virtual Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid id) { - return taskFactory.StartNew(() => - { - var schema = State.FindSchema(x => x.Id == id && !x.IsDeleted); + var schema = State.FindSchema(x => x.Id == id && !x.IsDeleted); - return (State.GetApp(), schema); - }); + return Task.FromResult((State.GetApp(), schema)); } public virtual Task GetAppAsync() { - return taskFactory.StartNew(() => - { - var value = State.GetApp(); + var result = State.GetApp(); - return value; - }); + return Task.FromResult(result); } public virtual Task> GetRulesAsync() { - return taskFactory.StartNew(() => - { - var value = State.FindRules(); + var result = State.FindRules(); - return value; - }); + return Task.FromResult(result); } public virtual Task> GetSchemasAsync() { - return taskFactory.StartNew(() => - { - var value = State.FindSchemas(x => !x.IsDeleted); + var result = State.FindSchemas(x => !x.IsDeleted); - return value; - }); + return Task.FromResult(result); } public virtual Task GetSchemaAsync(Guid id, bool provideDeleted = false) { - return taskFactory.StartNew(() => - { - var value = State.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); + var result = State.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); - return value; - }); + return Task.FromResult(result); } public virtual Task GetSchemaAsync(string name, bool provideDeleted = false) { - return taskFactory.StartNew(() => - { - var value = State.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); + var result = State.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); - return value; - }); + return Task.FromResult(result); } - public virtual Task HandleAsync(Envelope message) + public async virtual Task HandleAsync(Envelope message) { - return taskFactory.StartNew(async () => + if (exception != null) { - if (exception != null) + if (message.Payload is AppCreated) + { + exception = null; + } + else { - if (message.Payload is AppCreated) - { - exception = null; - } - else - { - throw exception; - } + throw exception; } + } + + if (message.Payload is AppEvent appEvent && (State.App == null || State.App.Id == appEvent.AppId.Id)) + { + try + { + State = State.Apply(message); - if (message.Payload is AppEvent appEvent) + await WriteStateAsync(); + } + catch (InconsistentStateException) { - if (State.App == null || State.App.Id == appEvent.AppId.Id) - { - try - { - State.Apply(message); - - await WriteStateAsync(); - } - catch (InconsistentStateException) - { - await ReadStateAsync(); - - State.Apply(message); - - await WriteStateAsync(); - } - } + await ReadStateAsync(); + + State = State.Apply(message); + + await WriteStateAsync(); } - }).Unwrap(); + } } } } diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs index c84f51a52..003ed92f6 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState.cs @@ -8,18 +8,20 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; 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; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; namespace Squidex.Domain.Apps.Read.State.Grains { - public sealed partial class AppStateGrainState + public sealed partial class AppStateGrainState : Cloneable { private FieldRegistry registry; @@ -27,10 +29,10 @@ namespace Squidex.Domain.Apps.Read.State.Grains public JsonAppEntity App { get; set; } [JsonProperty] - public Dictionary Rules { get; set; } + public ImmutableDictionary Rules { get; set; } = ImmutableDictionary.Empty; [JsonProperty] - public Dictionary Schemas { get; set; } + public ImmutableDictionary Schemas { get; set; } = ImmutableDictionary.Empty; public void SetRegistry(FieldRegistry registry) { @@ -57,21 +59,17 @@ namespace Squidex.Domain.Apps.Read.State.Grains return Rules?.Values.OfType().ToList() ?? new List(); } - public void Reset() + public AppStateGrainState Apply(Envelope envelope) { - Rules = new Dictionary(); - - Schemas = new Dictionary(); - } - - public void Apply(Envelope envelope) - { - this.DispatchAction(envelope.Payload, envelope.Headers); - - if (App != null) + return Clone(c => { - App.Etag = Guid.NewGuid().ToString(); - } + c.DispatchAction(envelope.Payload, envelope.Headers); + + if (c.App != null) + { + c.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 index cd17d3e72..a2477c590 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Apps.cs @@ -21,8 +21,6 @@ namespace Squidex.Domain.Apps.Read.State.Grains { public void On(AppCreated @event, EnvelopeHeaders headers) { - Reset(); - App = EntityMapper.Create(@event, headers, a => { SimpleMapper.Map(@event, a); diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs index ad6159f20..052dfbd75 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Rules.cs @@ -9,6 +9,7 @@ using System; using Squidex.Domain.Apps.Events.Rules; using Squidex.Domain.Apps.Events.Rules.Utils; +using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Domain.Apps.Read.State.Grains @@ -17,10 +18,12 @@ namespace Squidex.Domain.Apps.Read.State.Grains { public void On(RuleCreated @event, EnvelopeHeaders headers) { - Rules[@event.RuleId] = EntityMapper.Create(@event, headers, r => + var id = @event.RuleId; + + Rules.SetItem(id, EntityMapper.Create(@event, headers, r => { r.RuleDef = RuleEventDispatcher.Create(@event); - }); + })); } public void On(RuleUpdated @event, EnvelopeHeaders headers) @@ -56,7 +59,7 @@ namespace Squidex.Domain.Apps.Read.State.Grains { var id = @event.RuleId; - Rules[id] = Rules[id].Clone().Update(@event, headers, updater); + Rules = Rules.SetItem(id, x => x.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 index 2f919d859..494307a9b 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppStateGrainState_Schemas.cs @@ -11,6 +11,7 @@ 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; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Reflection; @@ -22,12 +23,14 @@ namespace Squidex.Domain.Apps.Read.State.Grains { public void On(SchemaCreated @event, EnvelopeHeaders headers) { - Schemas[@event.SchemaId.Id] = EntityMapper.Create(@event, headers, s => + var id = @event.SchemaId.Id; + + Schemas = Schemas.SetItem(id, EntityMapper.Create(@event, headers, s => { s.SchemaDef = SchemaEventDispatcher.Create(@event, registry); SimpleMapper.Map(@event, s); - }); + })); } public void On(SchemaPublished @event, EnvelopeHeaders headers) @@ -70,11 +73,6 @@ namespace Squidex.Domain.Apps.Read.State.Grains }); } - public void On(SchemaDeleted @event) - { - Schemas.Remove(@event.SchemaId.Id); - } - public void On(FieldAdded @event, EnvelopeHeaders headers) { UpdateSchema(@event, headers, s => @@ -149,11 +147,16 @@ namespace Squidex.Domain.Apps.Read.State.Grains UpdateSchema(@event, headers); } + public void On(SchemaDeleted @event, EnvelopeHeaders headers) + { + Schemas = Schemas.Remove(@event.SchemaId.Id); + } + private void UpdateSchema(SchemaEvent @event, EnvelopeHeaders headers, Action updater = null) { var id = @event.SchemaId.Id; - Schemas[id] = Schemas[id].Clone().Update(@event, headers, updater); + Schemas = Schemas.SetItem(id, x => x.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 index 26e43cb4a..3ab4f8cfa 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrain.cs @@ -10,40 +10,28 @@ 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.Grains { public sealed class AppUserGrain : StatefulObject { - private readonly TaskFactory taskFactory = new TaskFactory(new LimitedConcurrencyLevelTaskScheduler(1)); - public Task AddAppAsync(string appName) { - return taskFactory.StartNew(() => - { - State.AppNames.Add(appName); + State = State.AddApp(appName); - return WriteStateAsync(); - }).Unwrap(); + return WriteStateAsync(); } public Task RemoveAppAsync(string appName) { - return taskFactory.StartNew(() => - { - State.AppNames.Remove(appName); + State = State.RemoveApp(appName); - return WriteStateAsync(); - }).Unwrap(); + return WriteStateAsync(); } public Task> GetAppNamesAsync() { - return taskFactory.StartNew(() => - { - return State.AppNames.ToList(); - }); + return Task.FromResult(State.AppNames.ToList()); } } } diff --git a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs index 823969670..10da85cb0 100644 --- a/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs +++ b/src/Squidex.Domain.Apps.Read/State/Grains/AppUserGrainState.cs @@ -6,14 +6,25 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System.Collections.Immutable; using Newtonsoft.Json; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Read.State.Grains { - public sealed class AppUserGrainState + public sealed class AppUserGrainState : Cloneable { [JsonProperty] - public HashSet AppNames { get; set; } = new HashSet(); + public ImmutableHashSet AppNames { get; set; } = ImmutableHashSet.Empty; + + public AppUserGrainState AddApp(string appName) + { + return Clone(c => c.AppNames = c.AppNames.Add(appName)); + } + + public AppUserGrainState RemoveApp(string appName) + { + return Clone(c => c.AppNames = c.AppNames.Remove(appName)); + } } } diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index ea0f8362e..d0de8d2c2 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -8,12 +8,28 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Squidex.Infrastructure { public static class CollectionExtensions { + public static ImmutableDictionary SetItem(this ImmutableDictionary dictionary, TKey key, Func updater) + { + if (dictionary.TryGetValue(key, out var value)) + { + var newValue = updater(value); + + if (!Equals(newValue, value)) + { + return dictionary.SetItem(key, newValue); + } + } + + return dictionary; + } + public static bool TryGetValue(this IReadOnlyDictionary values, TKey key, out TBase item) where TValue : TBase { if (values.TryGetValue(key, out var value)) diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 0e7de800d..92fbf6503 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index d52d8be76..bd69cbf3c 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -17,6 +17,7 @@ namespace Squidex.Config.Web public static void AddMyMvc(this IServiceCollection services) { services.AddSingletonAs(); + services.AddSingletonAs(); services.AddSingletonAs(); diff --git a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs index b7547e3de..45237cd47 100644 --- a/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Collections.Immutable; using Xunit; namespace Squidex.Infrastructure @@ -268,5 +269,35 @@ namespace Squidex.Infrastructure Assert.Equal(source, target); } + + [Fact] + public void Should_return_same_dictionary_if_item_to_replace_not_found() + { + var dict_0 = ImmutableDictionary.Empty; + var dict_1 = dict_0.SetItem(1, x => x); + + Assert.Same(dict_0, dict_1); + } + + [Fact] + public void Should_return_same_dictionary_if_replaced_item_is_same() + { + var dict_0 = ImmutableDictionary.Empty; + var dict_1 = dict_0.SetItem(1, 1); + var dict_2 = dict_1.SetItem(1, x => x); + + Assert.Same(dict_1, dict_2); + } + + [Fact] + public void Should_return_new_dictionary_if_updated_item_is_different() + { + var dict_0 = ImmutableDictionary.Empty; + var dict_1 = dict_0.SetItem(2, 2); + var dict_2 = dict_1.SetItem(2, x => 2 * x); + + Assert.NotSame(dict_1, dict_2); + Assert.Equal(4, dict_2[2]); + } } } \ No newline at end of file