first draft of message handler

This commit is contained in:
2020-10-06 16:00:27 +02:00
parent cced92d89b
commit 813b9c259b
19 changed files with 395 additions and 14 deletions
+8 -7
View File
@@ -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<T>()
{
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();
+7 -1
View File
@@ -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; }
}
}
@@ -0,0 +1,14 @@
using Raven.Client.Documents.Session;
using zero.Core.Messages;
namespace zero.Core.Entities.Messages
{
public abstract class EntityMessageBase<T> : IMessage
{
public IAsyncDocumentSession Session { get; set; }
public string Id { get; set; }
public T Model { get; set; }
}
}
@@ -0,0 +1,24 @@
namespace zero.Core.Entities.Messages
{
public class EntityCreatedMessage : EntityMessageBase<IZeroEntity>
{
}
public class EntityUpdatedMessage : EntityMessageBase<IZeroEntity>
{
}
public class EntityDeletedMessage : EntityMessageBase<IZeroEntity>
{
}
public class EntitySavedMessage<T> : EntityMessageBase<T>
{
public bool IsCreate { get; set; }
public bool IsDelete { get; set; }
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace zero.Core.Messages
{
public interface IMessage
{
}
}
+30
View File
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace zero.Core.Messages
{
/// <summary>
/// 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)
/// </summary>
public interface IMessageHandler<TMessage> where TMessage : IMessage
{
/// <summary>
/// Method to invoke when a message of type TMessage is published
/// </summary>
Task Handle(TMessage message);
}
/// <summary>
/// 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)
/// </summary>
public interface IBatchMessageHandler<TMessage> : IMessageHandler<TMessage> where TMessage : IMessage
{
/// <summary>
/// Method to invoke when a batch of messages of type TMessage are published
/// </summary>
Task HandleBatchAsync(IReadOnlyCollection<TMessage> message);
}
}
+57
View File
@@ -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<IMessageSubscription> Subscription = new ConcurrentBag<IMessageSubscription>();
readonly IServiceProvider ServiceProvider;
public MessageAggregator(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
/// <inheritdoc />
public async Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage
{
IEnumerable<IMessageSubscription> subscriptions = Subscription.Where(s => s.CanDeliver<TMessage>());
foreach (IMessageSubscription subscription in subscriptions)
{
await subscription.Deliver(ServiceProvider, message);
}
}
/// <inheritdoc />
public void Subscribe<TMessage, TMessageHandler>()
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>
{
Subscription.Add(new MessageSubscription<TMessage, TMessageHandler>());
}
}
public interface IMessageAggregator
{
/// <summary>
/// Publishes the specified message, invoking any handlers subscribed to the message.
/// </summary>
Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage;
/// <summary>
/// Subscribes the specified handler to the spified message type
/// </summary>
void Subscribe<TMessage, TMessageHandler>()
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>;
}
}
+40
View File
@@ -0,0 +1,40 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace zero.Core.Messages
{
internal class MessageSubscription<TMessage, TMessageHandler> : IMessageSubscription
where TMessage : class, IMessage
where TMessageHandler : IMessageHandler<TMessage>
{
public bool CanDeliver<T>()
{
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<TMessageHandler>();
await handler.Handle((TMessage)message);
}
}
public interface IMessageSubscription
{
bool CanDeliver<TMessage>();
Task Deliver(IServiceProvider serviceProvider, object message);
}
}
+1
View File
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using zero.Core.Options;
namespace zero.Core.Plugins
+9 -1
View File
@@ -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<string> LocalizationPaths { get; }
}
public interface IZeroPluginStartup
{
Task Startup();
}
}
+4
View File
@@ -24,4 +24,8 @@
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Entities\Messages\Applications\" />
</ItemGroup>
</Project>
+90
View File
@@ -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<T> : IMessageHandler<EntitySavedMessage<T>> where T : IZeroEntity
{
public virtual async Task Handle(EntitySavedMessage<T> 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<T> inheritedModels = new List<T>();
IList<IApplication> apps = await session.Query<IApplication>().Where(x => x.Id != sharedId).ToListAsync();
if (!isCreate)
{
string id = model.Id;
inheritedModels = await session.Query<T>().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);
}
}
}
}
@@ -0,0 +1,14 @@
using Raven.Client.Documents.Session;
using zero.Core.Entities;
namespace zero.Debug.Sync
{
public class CountryBlueprintHandler : BlueprintHandler<ICountry>
{
protected override void OnBlueprintCreate(ICountry blueprint, ICountry model)
{
model.Code = blueprint.Code;
model.IsPreferred = blueprint.IsPreferred;
}
}
}
@@ -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<IProperty>
{
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<IPropertyValue> 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;
}
}
}
+4 -2
View File
@@ -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<TestPluginStartup>();
services.Configure<ZeroCommerceOptions>(opts =>
{
opts.Documents.Add<InvoiceDocument>();
@@ -50,7 +52,7 @@ namespace zero.TestData
opts.ChannelFeatures.Add("channel.altFrontend", "Alternative header", "Render a simplified header in the frontend");
});
//services.Replace<IChannel, SalesChannel>();
services.AddTransient<CountryBlueprintHandler>(); // TODO auto-register handlers
services.AddTransient<ITestService, TestService>();
}
}
+33
View File
@@ -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<EntitySavedMessage<ICountry>, CountryBlueprintHandler>();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
+1 -1
View File
@@ -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
});
@@ -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<IMessageAggregator, MessageAggregator>();
services.AddTransient<IApplication, Application>();
services.AddTransient<ICountry, Country>();
@@ -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;