From d42ac940be2be4953dec8a9853fc5801c48dff29 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 13 Feb 2022 21:04:12 +0100 Subject: [PATCH] feat(core): add command pre and post events --- .../Client/BaseNostaleClient.cs | 4 +- .../Commands/CommandProcessor.cs | 126 +++++++++++++++++- .../Commands/IPostCommandExecutionEvent.cs | 36 +++++ .../Commands/IPreCommandExecutionEvent.cs | 39 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 25 ++++ .../Stateful/StatefulPreExecutionEvent.cs | 11 +- 6 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs create mode 100644 Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs diff --git a/Core/NosSmooth.Core/Client/BaseNostaleClient.cs b/Core/NosSmooth.Core/Client/BaseNostaleClient.cs index dac1939..e5db43e 100644 --- a/Core/NosSmooth.Core/Client/BaseNostaleClient.cs +++ b/Core/NosSmooth.Core/Client/BaseNostaleClient.cs @@ -68,6 +68,6 @@ public abstract class BaseNostaleClient : INostaleClient } /// - public Task SendCommandAsync(ICommand command, CancellationToken ct = default) => - _commandProcessor.ProcessCommand(command, ct); + public async Task SendCommandAsync(ICommand command, CancellationToken ct = default) + => await _commandProcessor.ProcessCommand(this, command, ct); } diff --git a/Core/NosSmooth.Core/Commands/CommandProcessor.cs b/Core/NosSmooth.Core/Commands/CommandProcessor.cs index 37a492e..4392aa3 100644 --- a/Core/NosSmooth.Core/Commands/CommandProcessor.cs +++ b/Core/NosSmooth.Core/Commands/CommandProcessor.cs @@ -5,11 +5,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using NosSmooth.Core.Client; using NosSmooth.Core.Errors; +using NosSmooth.Core.Packets; +using NosSmooth.Packets; using Remora.Results; namespace NosSmooth.Core.Commands; @@ -34,11 +39,12 @@ public class CommandProcessor /// /// Processes the given command, calling its handler or returning error. /// + /// The NosTale client. /// The command to process. /// The cancellation token for cancelling the operation. /// A result that may or may not have succeeded. /// Thrown on critical error. - public Task ProcessCommand(ICommand command, CancellationToken ct = default) + public Task ProcessCommand(INostaleClient client, ICommand command, CancellationToken ct = default) { var processMethod = GetType().GetMethod ( @@ -53,19 +59,129 @@ public class CommandProcessor var boundProcessMethod = processMethod.MakeGenericMethod(command.GetType()); - return (Task)boundProcessMethod.Invoke(this, new object[] { command, ct })!; + return (Task)boundProcessMethod.Invoke(this, new object[] { client, command, ct })!; } - private Task DispatchCommandHandler(TCommand command, CancellationToken ct = default) + private async Task DispatchCommandHandler + ( + INostaleClient client, + TCommand command, + CancellationToken ct = default + ) where TCommand : class, ICommand { using var scope = _provider.CreateScope(); + var beforeResult = await ExecuteBeforeExecutionAsync(scope.ServiceProvider, client, command, ct); + if (!beforeResult.IsSuccess) + { + return beforeResult; + } + var commandHandler = scope.ServiceProvider.GetService>(); if (commandHandler is null) { - return Task.FromResult(Result.FromError(new CommandHandlerNotFound(command.GetType()))); + var result = Result.FromError(new CommandHandlerNotFound(command.GetType())); + var afterExecutionResult = await ExecuteAfterExecutionAsync + ( + scope.ServiceProvider, + client, + command, + result, + ct + ); + if (!afterExecutionResult.IsSuccess) + { + return new AggregateError(result, afterExecutionResult); + } + + return result; } - return commandHandler.HandleCommand(command, ct); + var handlerResult = await commandHandler.HandleCommand(command, ct); + var afterResult = await ExecuteAfterExecutionAsync + ( + scope.ServiceProvider, + client, + command, + handlerResult, + ct + ); + + if (!afterResult.IsSuccess && !handlerResult.IsSuccess) + { + return new AggregateError(handlerResult, afterResult); + } + + if (!handlerResult.IsSuccess) + { + return handlerResult; + } + + return afterResult; + } + + private async Task ExecuteBeforeExecutionAsync + ( + IServiceProvider services, + INostaleClient client, + TCommand command, + CancellationToken ct + ) + where TCommand : ICommand + { + var results = await Task.WhenAll + ( + services.GetServices() + .Select(x => x.ExecuteBeforeCommandAsync(client, command, ct)) + ); + + var errorResults = new List(); + foreach (var result in results) + { + if (!result.IsSuccess) + { + errorResults.Add(result); + } + } + + return errorResults.Count switch + { + 1 => errorResults[0], + 0 => Result.FromSuccess(), + _ => new AggregateError(errorResults.Cast().ToArray()) + }; + } + + private async Task ExecuteAfterExecutionAsync + ( + IServiceProvider services, + INostaleClient client, + TCommand command, + Result handlerResult, + CancellationToken ct + ) + where TCommand : ICommand + { + var results = await Task.WhenAll + ( + services.GetServices() + .Select(x => x.ExecuteAfterCommandAsync(client, command, handlerResult, ct)) + ); + + var errorResults = new List(); + foreach (var result in results) + { + if (!result.IsSuccess) + { + errorResults.Add(result); + } + } + + return errorResults.Count switch + { + 1 => errorResults[0], + 0 => Result.FromSuccess(), + _ => new AggregateError(errorResults.Cast().ToArray()) + }; } } \ No newline at end of file diff --git a/Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs b/Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs new file mode 100644 index 0000000..85573eb --- /dev/null +++ b/Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs @@ -0,0 +1,36 @@ +// +// IPostCommandExecutionEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; +using NosSmooth.Core.Client; +using Remora.Results; + +namespace NosSmooth.Core.Commands; + +/// +/// Event executed after command handler. +/// +public interface IPostCommandExecutionEvent +{ + /// + /// Execute the command post execution event. + /// + /// The NosTale client. + /// The command. + /// The result from the command handler. + /// The cancellation token for cancelling the operation. + /// The type of the command. + /// A result that may or may not succeed. + public Task ExecuteAfterCommandAsync + ( + INostaleClient client, + TCommand command, + Result handlerResult, + CancellationToken ct = default + ) + where TCommand : ICommand; +} \ No newline at end of file diff --git a/Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs b/Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs new file mode 100644 index 0000000..d233a68 --- /dev/null +++ b/Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs @@ -0,0 +1,39 @@ +// +// IPreCommandExecutionEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Threading.Tasks; +using NosSmooth.Core.Client; +using NosSmooth.Core.Packets; +using NosSmooth.Packets; +using Remora.Results; + +namespace NosSmooth.Core.Commands; + +/// +/// Event executed prior to command handler. +/// +public interface IPreCommandExecutionEvent +{ + /// + /// Execute the command pre execution event. + /// + /// + /// If an error is returned, the command handler won't be called. + /// + /// The NosTale client. + /// The command. + /// The cancellation token for cancelling the operation. + /// The type of the command. + /// A result that may or may not succeed. + public Task ExecuteBeforeCommandAsync + ( + INostaleClient client, + TCommand command, + CancellationToken ct = default + ) + where TCommand : ICommand; +} \ No newline at end of file diff --git a/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs index 67cfaa4..745ed66 100644 --- a/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs @@ -171,6 +171,30 @@ public static class ServiceCollectionExtensions return serviceCollection; } + /// + /// Add the given pre execution event that will be executed before the command handler. + /// + /// The service collection. + /// The pre execution event type. + /// The collection. + public static IServiceCollection AddPreCommandExecutionEvent(this IServiceCollection serviceCollection) + where TEvent : class, IPreCommandExecutionEvent + { + return serviceCollection.AddScoped(); + } + + /// + /// Add the given post execution event that will be executed after the command handler. + /// + /// The service collection. + /// The pre execution event type. + /// The collection. + public static IServiceCollection AddPostCommandExecutionEvent(this IServiceCollection serviceCollection) + where TEvent : class, IPostCommandExecutionEvent + { + return serviceCollection.AddScoped(); + } + /// /// Add the given pre execution event that will be executed before the packet responders. /// @@ -207,6 +231,7 @@ public static class ServiceCollectionExtensions .AddScoped() .AddSingleton() .AddPreExecutionEvent() + .AddPreCommandExecutionEvent() .AddScoped ( p => diff --git a/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs b/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs index 0771abc..4975bcf 100644 --- a/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs +++ b/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NosSmooth.Core.Client; +using NosSmooth.Core.Commands; using NosSmooth.Core.Packets; using NosSmooth.Packets; using Remora.Results; @@ -18,7 +19,7 @@ namespace NosSmooth.Core.Stateful; /// /// Event that injects stateful entities into the scope. /// -public class StatefulPreExecutionEvent : IPreExecutionEvent +public class StatefulPreExecutionEvent : IPreExecutionEvent, IPreCommandExecutionEvent { private readonly StatefulInjector _injector; @@ -39,4 +40,12 @@ public class StatefulPreExecutionEvent : IPreExecutionEvent _injector.Client = client; return Task.FromResult(Result.FromSuccess()); } + + /// + public Task ExecuteBeforeCommandAsync(INostaleClient client, TCommand command, CancellationToken ct = default) + where TCommand : ICommand + { + _injector.Client = client; + return Task.FromResult(Result.FromSuccess()); + } } \ No newline at end of file -- 2.49.0