From 813b9c259baf6f2cf1d0bf87e6c4c5caafcfb33c Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Tue, 6 Oct 2020 16:00:27 +0200 Subject: [PATCH] first draft of message handler --- zero.Core/Api/BackofficeApi.cs | 15 ++-- zero.Core/Api/BackofficeStore.cs | 8 +- .../Entities/Messages/EntityMessageBase.cs | 14 +++ zero.Core/Entities/Messages/EntityMessages.cs | 24 +++++ zero.Core/Messages/IMessage.cs | 6 ++ zero.Core/Messages/IMessageHandler.cs | 30 +++++++ zero.Core/Messages/MessageAggregator.cs | 57 ++++++++++++ zero.Core/Messages/MessageSubscription.cs | 40 +++++++++ zero.Core/Plugins/IZeroPlugin.cs | 1 + zero.Core/Plugins/ZeroPluginOptions.cs | 10 ++- zero.Core/zero.Core.csproj | 4 + zero.Debug/Sync/BlueprintHandler.cs | 90 +++++++++++++++++++ zero.Debug/Sync/CountryBlueprintHandler.cs | 14 +++ zero.Debug/Sync/PropertyBlueprintHandler.cs | 51 +++++++++++ zero.Debug/TestData/TestPlugin.cs | 6 +- zero.Debug/TestData/TestPluginStartup.cs | 33 +++++++ zero.Web.UI/App/editor/editor.vue | 2 +- zero.Web/Defaults/ZeroBackofficePlugin.cs | 2 + zero.Web/ZeroApplicationBuilderExtensions.cs | 2 - 19 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 zero.Core/Entities/Messages/EntityMessageBase.cs create mode 100644 zero.Core/Entities/Messages/EntityMessages.cs create mode 100644 zero.Core/Messages/IMessage.cs create mode 100644 zero.Core/Messages/IMessageHandler.cs create mode 100644 zero.Core/Messages/MessageAggregator.cs create mode 100644 zero.Core/Messages/MessageSubscription.cs create mode 100644 zero.Debug/Sync/BlueprintHandler.cs create mode 100644 zero.Debug/Sync/CountryBlueprintHandler.cs create mode 100644 zero.Debug/Sync/PropertyBlueprintHandler.cs create mode 100644 zero.Debug/TestData/TestPluginStartup.cs diff --git a/zero.Core/Api/BackofficeApi.cs b/zero.Core/Api/BackofficeApi.cs index 3e44ab74..7c2aaa3c 100644 --- a/zero.Core/Api/BackofficeApi.cs +++ b/zero.Core/Api/BackofficeApi.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using zero.Core.Attributes; using zero.Core.Entities; +using zero.Core.Entities.Messages; using zero.Core.Extensions; using zero.Core.Sync; using zero.Core.Utils; @@ -219,14 +220,14 @@ namespace zero.Core.Api meta?.Invoke(session.Advanced.GetMetadataFor(model)); - if (isCreate) + await Backoffice.Messages.Publish(new EntitySavedMessage() { - await new SyncPseudo().OnCreate(session, model); - } - else - { - await new SyncPseudo().OnUpdate(session, model); - } + Id = model.Id, + IsCreate = isCreate, + IsDelete = false, + Model = model, + Session = session + }); await session.SaveChangesAsync(); diff --git a/zero.Core/Api/BackofficeStore.cs b/zero.Core/Api/BackofficeStore.cs index e6beab14..57b0bfdf 100644 --- a/zero.Core/Api/BackofficeStore.cs +++ b/zero.Core/Api/BackofficeStore.cs @@ -1,4 +1,5 @@ using Raven.Client.Documents; +using zero.Core.Messages; using zero.Core.Options; namespace zero.Core.Api @@ -13,13 +14,16 @@ namespace zero.Core.Api public IAuthenticationApi Auth { get; private set; } + public IMessageAggregator Messages { get; private set; } - public BackofficeStore(IDocumentStore raven, IApplicationContext appContext, IZeroOptions options, IAuthenticationApi authenticationApi) + + public BackofficeStore(IDocumentStore raven, IApplicationContext appContext, IZeroOptions options, IAuthenticationApi authenticationApi, IMessageAggregator messages) { Raven = raven; AppContext = appContext; Options = options; Auth = authenticationApi; + Messages = messages; } } @@ -33,5 +37,7 @@ namespace zero.Core.Api IZeroOptions Options { get; } IAuthenticationApi Auth { get; } + + IMessageAggregator Messages { get; } } } \ No newline at end of file diff --git a/zero.Core/Entities/Messages/EntityMessageBase.cs b/zero.Core/Entities/Messages/EntityMessageBase.cs new file mode 100644 index 00000000..7bfe21bb --- /dev/null +++ b/zero.Core/Entities/Messages/EntityMessageBase.cs @@ -0,0 +1,14 @@ +using Raven.Client.Documents.Session; +using zero.Core.Messages; + +namespace zero.Core.Entities.Messages +{ + public abstract class EntityMessageBase : IMessage + { + public IAsyncDocumentSession Session { get; set; } + + public string Id { get; set; } + + public T Model { get; set; } + } +} diff --git a/zero.Core/Entities/Messages/EntityMessages.cs b/zero.Core/Entities/Messages/EntityMessages.cs new file mode 100644 index 00000000..130da7f1 --- /dev/null +++ b/zero.Core/Entities/Messages/EntityMessages.cs @@ -0,0 +1,24 @@ +namespace zero.Core.Entities.Messages +{ + public class EntityCreatedMessage : EntityMessageBase + { + + } + + public class EntityUpdatedMessage : EntityMessageBase + { + + } + + public class EntityDeletedMessage : EntityMessageBase + { + + } + + public class EntitySavedMessage : EntityMessageBase + { + public bool IsCreate { get; set; } + + public bool IsDelete { get; set; } + } +} diff --git a/zero.Core/Messages/IMessage.cs b/zero.Core/Messages/IMessage.cs new file mode 100644 index 00000000..667ccafc --- /dev/null +++ b/zero.Core/Messages/IMessage.cs @@ -0,0 +1,6 @@ +namespace zero.Core.Messages +{ + public interface IMessage + { + } +} diff --git a/zero.Core/Messages/IMessageHandler.cs b/zero.Core/Messages/IMessageHandler.cs new file mode 100644 index 00000000..1c323463 --- /dev/null +++ b/zero.Core/Messages/IMessageHandler.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace zero.Core.Messages +{ + /// + /// Indicates a handler that can perform an action when a message of + /// a certain type is received. (could be an interface rather than concrete type) + /// + public interface IMessageHandler where TMessage : IMessage + { + /// + /// Method to invoke when a message of type TMessage is published + /// + Task Handle(TMessage message); + } + + + /// + /// Indicates a handler that can perform an action when a message of + /// a certain type is received. (could be an interface rather than concrete type) + /// + public interface IBatchMessageHandler : IMessageHandler where TMessage : IMessage + { + /// + /// Method to invoke when a batch of messages of type TMessage are published + /// + Task HandleBatchAsync(IReadOnlyCollection message); + } +} diff --git a/zero.Core/Messages/MessageAggregator.cs b/zero.Core/Messages/MessageAggregator.cs new file mode 100644 index 00000000..082005d8 --- /dev/null +++ b/zero.Core/Messages/MessageAggregator.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace zero.Core.Messages +{ + public class MessageAggregator : IMessageAggregator + { + readonly ConcurrentBag Subscription = new ConcurrentBag(); + readonly IServiceProvider ServiceProvider; + + + public MessageAggregator(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + + /// + public async Task Publish(TMessage message) where TMessage : class, IMessage + { + IEnumerable subscriptions = Subscription.Where(s => s.CanDeliver()); + + foreach (IMessageSubscription subscription in subscriptions) + { + await subscription.Deliver(ServiceProvider, message); + } + } + + + /// + public void Subscribe() + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler + { + Subscription.Add(new MessageSubscription()); + } + } + + + public interface IMessageAggregator + { + /// + /// Publishes the specified message, invoking any handlers subscribed to the message. + /// + Task Publish(TMessage message) where TMessage : class, IMessage; + + /// + /// Subscribes the specified handler to the spified message type + /// + void Subscribe() + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler; + } +} diff --git a/zero.Core/Messages/MessageSubscription.cs b/zero.Core/Messages/MessageSubscription.cs new file mode 100644 index 00000000..bbce2ee0 --- /dev/null +++ b/zero.Core/Messages/MessageSubscription.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace zero.Core.Messages +{ + internal class MessageSubscription : IMessageSubscription + where TMessage : class, IMessage + where TMessageHandler : IMessageHandler + { + public bool CanDeliver() + { + return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T)); + } + + public async Task Deliver(IServiceProvider serviceProvider, object message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + if (!(message is TMessage)) + { + throw new ArgumentException($"{ nameof(message) } must be of type '{typeof(TMessage).FullName}'"); + } + + var handler = serviceProvider.GetRequiredService(); + await handler.Handle((TMessage)message); + } + } + + + public interface IMessageSubscription + { + bool CanDeliver(); + + Task Deliver(IServiceProvider serviceProvider, object message); + } +} diff --git a/zero.Core/Plugins/IZeroPlugin.cs b/zero.Core/Plugins/IZeroPlugin.cs index 9150b427..b5ce5738 100644 --- a/zero.Core/Plugins/IZeroPlugin.cs +++ b/zero.Core/Plugins/IZeroPlugin.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; using zero.Core.Options; namespace zero.Core.Plugins diff --git a/zero.Core/Plugins/ZeroPluginOptions.cs b/zero.Core/Plugins/ZeroPluginOptions.cs index 6b930469..60668132 100644 --- a/zero.Core/Plugins/ZeroPluginOptions.cs +++ b/zero.Core/Plugins/ZeroPluginOptions.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; namespace zero.Core.Plugins { @@ -20,4 +22,10 @@ namespace zero.Core.Plugins List LocalizationPaths { get; } } + + + public interface IZeroPluginStartup + { + Task Startup(); + } } diff --git a/zero.Core/zero.Core.csproj b/zero.Core/zero.Core.csproj index bfa939db..bf7ab36f 100644 --- a/zero.Core/zero.Core.csproj +++ b/zero.Core/zero.Core.csproj @@ -24,4 +24,8 @@ + + + + \ No newline at end of file diff --git a/zero.Debug/Sync/BlueprintHandler.cs b/zero.Debug/Sync/BlueprintHandler.cs new file mode 100644 index 00000000..e9051256 --- /dev/null +++ b/zero.Debug/Sync/BlueprintHandler.cs @@ -0,0 +1,90 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using zero.Core; +using zero.Core.Api; +using zero.Core.Entities; +using zero.Core.Entities.Messages; +using zero.Core.Extensions; +using zero.Core.Messages; + +namespace zero.Debug.Sync +{ + public abstract class BlueprintHandler : IMessageHandler> where T : IZeroEntity + { + public virtual async Task Handle(EntitySavedMessage message) + { + await CreateBlueprints(message.Session, message.Model, message.IsCreate); + } + + + protected virtual Task OnBlueprintCreateAsync(T blueprint, T model) + { + return Task.CompletedTask; + } + + + protected virtual void OnBlueprintCreate(T blueprint, T model) + { + return; + } + + + protected virtual async Task CreateBlueprints(IAsyncDocumentSession session, T model, bool isCreate = false) + { + string sharedId = Constants.Database.SharedAppId; + + if (model.AppId != sharedId) + { + return; + } + + IList inheritedModels = new List(); + IList apps = await session.Query().Where(x => x.Id != sharedId).ToListAsync(); + + if (!isCreate) + { + string id = model.Id; + inheritedModels = await session.Query().Where(x => x.Blueprint != null && x.Blueprint.Id == id).ToListAsync(); + } + + foreach (IApplication app in apps) + { + bool exists = true; + T inheritedModel = inheritedModels.FirstOrDefault(x => x.AppId == app.Id); + + // the model does not yet exist in the app, so we need to create it + if (inheritedModel == null) + { + exists = false; + inheritedModel = model.Clone(); + inheritedModel.Id = null; + } + + inheritedModel.AppId = app.Id; + inheritedModel.LastModifiedById = model.LastModifiedById; + inheritedModel.LastModifiedDate = model.LastModifiedDate; + inheritedModel.Blueprint = new BlueprintConfiguration() + { + Id = model.Id + }; + + // we need to override allowed properties in the inherited model + if (exists) + { + inheritedModel.Name = model.Name; + inheritedModel.Alias = Safenames.Alias(model.Name); + inheritedModel.IsActive = model.IsActive; + inheritedModel.Sort = model.Sort; + await OnBlueprintCreateAsync(model, inheritedModel); + OnBlueprintCreate(model, inheritedModel); + } + + await session.StoreAsync(inheritedModel); + } + } + } +} diff --git a/zero.Debug/Sync/CountryBlueprintHandler.cs b/zero.Debug/Sync/CountryBlueprintHandler.cs new file mode 100644 index 00000000..a9c1e154 --- /dev/null +++ b/zero.Debug/Sync/CountryBlueprintHandler.cs @@ -0,0 +1,14 @@ +using Raven.Client.Documents.Session; +using zero.Core.Entities; + +namespace zero.Debug.Sync +{ + public class CountryBlueprintHandler : BlueprintHandler + { + protected override void OnBlueprintCreate(ICountry blueprint, ICountry model) + { + model.Code = blueprint.Code; + model.IsPreferred = blueprint.IsPreferred; + } + } +} diff --git a/zero.Debug/Sync/PropertyBlueprintHandler.cs b/zero.Debug/Sync/PropertyBlueprintHandler.cs new file mode 100644 index 00000000..342ab5e9 --- /dev/null +++ b/zero.Debug/Sync/PropertyBlueprintHandler.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using zero.Commerce.Entities; +using zero.Core.Api; + +namespace zero.Debug.Sync +{ + public class PropertyBlueprintHandler : BlueprintHandler + { + protected override void OnBlueprintCreate(IProperty blueprint, IProperty model) + { + model.Description = blueprint.Description; + model.Limit = blueprint.Limit; + model.ForFilter = blueprint.ForFilter; + model.AllowText = blueprint.AllowText; + model.IconSet = blueprint.IconSet; + model.Type = blueprint.Type; + model.SortingMethod = blueprint.SortingMethod; + + List values = blueprint.Values.ToList(); + string[] valueIds = values.Select(x => x.Id).ToArray(); + + foreach (IPropertyValue value in values) + { + string desyncKey = $"Values[{value.Id}]."; + string[] desynced = model.Blueprint.Desync.Where(x => x.StartsWith(desyncKey)).ToArray(); + IPropertyValue match = model.Values.FirstOrDefault(x => x.Id == value.Id); + + if (match != null) + { + if (desynced.Contains(desyncKey + nameof(value.Value))) + { + value.Value = match.Value; + value.Alias = Safenames.Alias(value.Value); + } + if (value is ColorPropertyValue && desynced.Contains(desyncKey + "Hex")) + { + ((ColorPropertyValue)value).Hex = ((ColorPropertyValue)match).Hex; + } + } + } + + foreach (IPropertyValue value in model.Values.Where(x => !valueIds.Contains(x.Id))) + { + values.Add(value); + } + + model.Values = values; + } + } +} diff --git a/zero.Debug/TestData/TestPlugin.cs b/zero.Debug/TestData/TestPlugin.cs index c20ea0ef..c7142728 100644 --- a/zero.Debug/TestData/TestPlugin.cs +++ b/zero.Debug/TestData/TestPlugin.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using zero.Commerce.Options; using zero.Core.Options; using zero.Core.Plugins; +using zero.Debug.Sync; using zero.Debug.TestData; namespace zero.TestData @@ -13,7 +14,6 @@ namespace zero.TestData public void Configure(IZeroPluginOptions plugin, IZeroOptions zero) { plugin.Name = "Test Plugin"; - //ISection spaceSection = zero.Sections.GetAllItems().FirstOrDefault(x => x.Alias == Constants.Sections.Spaces); //zero.Sections.Remove(spaceSection); @@ -41,6 +41,8 @@ namespace zero.TestData public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { + services.AddHostedService(); + services.Configure(opts => { opts.Documents.Add(); @@ -50,7 +52,7 @@ namespace zero.TestData opts.ChannelFeatures.Add("channel.altFrontend", "Alternative header", "Render a simplified header in the frontend"); }); - //services.Replace(); + services.AddTransient(); // TODO auto-register handlers services.AddTransient(); } } diff --git a/zero.Debug/TestData/TestPluginStartup.cs b/zero.Debug/TestData/TestPluginStartup.cs new file mode 100644 index 00000000..042c8adb --- /dev/null +++ b/zero.Debug/TestData/TestPluginStartup.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; +using zero.Core.Entities; +using zero.Core.Entities.Messages; +using zero.Core.Messages; +using zero.Debug.Sync; + +namespace zero.TestData +{ + public class TestPluginStartup : IHostedService + { + IMessageAggregator Messages; + + + public TestPluginStartup(IMessageAggregator messages) + { + Messages = messages; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Messages.Subscribe, CountryBlueprintHandler>(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/zero.Web.UI/App/editor/editor.vue b/zero.Web.UI/App/editor/editor.vue index 6a1e791b..cb8bffbf 100644 --- a/zero.Web.UI/App/editor/editor.vue +++ b/zero.Web.UI/App/editor/editor.vue @@ -226,7 +226,7 @@ let params = this.$route.params; params.id = blueprint.id; - this.$router.replace({ + this.$router.push({ name: this.$route.name, params: params }); diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs index 0cad09a5..d4d7aef6 100644 --- a/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using zero.Core.Api; using zero.Core.Entities; using zero.Core.Extensions; +using zero.Core.Messages; using zero.Core.Options; using zero.Core.Plugins; using zero.Core.Validation; @@ -18,6 +19,7 @@ namespace zero.Web.Defaults { //services.AddAll(typeof(IValidator<>), ServiceLifetime.Scoped); //services.AddAll(typeof(IValidator), ServiceLifetime.Scoped); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); diff --git a/zero.Web/ZeroApplicationBuilderExtensions.cs b/zero.Web/ZeroApplicationBuilderExtensions.cs index 912d5946..66f2ef24 100644 --- a/zero.Web/ZeroApplicationBuilderExtensions.cs +++ b/zero.Web/ZeroApplicationBuilderExtensions.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.SpaServices.Webpack; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using System; using System.IO; -using zero.Core; using zero.Core.Extensions; using zero.Core.Middlewares; using zero.Core.Options;