M Core/NosSmooth.Core/Client/BaseNostaleClient.cs => Core/NosSmooth.Core/Client/BaseNostaleClient.cs +2 -2
@@ 68,6 68,6 @@ public abstract class BaseNostaleClient : INostaleClient
}
/// <inheritdoc />
- public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default) =>
- _commandProcessor.ProcessCommand(command, ct);
+ public async Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
+ => await _commandProcessor.ProcessCommand(this, command, ct);
}
M Core/NosSmooth.Core/Commands/CommandProcessor.cs => Core/NosSmooth.Core/Commands/CommandProcessor.cs +121 -5
@@ 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
/// <summary>
/// Processes the given command, calling its handler or returning error.
/// </summary>
+ /// <param name="client">The NosTale client.</param>
/// <param name="command">The command to process.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A result that may or may not have succeeded.</returns>
/// <exception cref="InvalidOperationException">Thrown on critical error.</exception>
- public Task<Result> ProcessCommand(ICommand command, CancellationToken ct = default)
+ public Task<Result> 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<Result>)boundProcessMethod.Invoke(this, new object[] { command, ct })!;
+ return (Task<Result>)boundProcessMethod.Invoke(this, new object[] { client, command, ct })!;
}
- private Task<Result> DispatchCommandHandler<TCommand>(TCommand command, CancellationToken ct = default)
+ private async Task<Result> DispatchCommandHandler<TCommand>
+ (
+ 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<ICommandHandler<TCommand>>();
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<Result> ExecuteBeforeExecutionAsync<TCommand>
+ (
+ IServiceProvider services,
+ INostaleClient client,
+ TCommand command,
+ CancellationToken ct
+ )
+ where TCommand : ICommand
+ {
+ var results = await Task.WhenAll
+ (
+ services.GetServices<IPreCommandExecutionEvent>()
+ .Select(x => x.ExecuteBeforeCommandAsync(client, command, ct))
+ );
+
+ var errorResults = new List<Result>();
+ 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<IResult>().ToArray())
+ };
+ }
+
+ private async Task<Result> ExecuteAfterExecutionAsync<TCommand>
+ (
+ IServiceProvider services,
+ INostaleClient client,
+ TCommand command,
+ Result handlerResult,
+ CancellationToken ct
+ )
+ where TCommand : ICommand
+ {
+ var results = await Task.WhenAll
+ (
+ services.GetServices<IPostCommandExecutionEvent>()
+ .Select(x => x.ExecuteAfterCommandAsync(client, command, handlerResult, ct))
+ );
+
+ var errorResults = new List<Result>();
+ 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<IResult>().ToArray())
+ };
}
}=
\ No newline at end of file
A Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs => Core/NosSmooth.Core/Commands/IPostCommandExecutionEvent.cs +36 -0
@@ 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;
+
+/// <summary>
+/// Event executed after command handler.
+/// </summary>
+public interface IPostCommandExecutionEvent
+{
+ /// <summary>
+ /// Execute the command post execution event.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="handlerResult">The result from the command handler.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <typeparam name="TCommand">The type of the command.</typeparam>
+ /// <returns>A result that may or may not succeed.</returns>
+ public Task<Result> ExecuteAfterCommandAsync<TCommand>
+ (
+ INostaleClient client,
+ TCommand command,
+ Result handlerResult,
+ CancellationToken ct = default
+ )
+ where TCommand : ICommand;
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs => Core/NosSmooth.Core/Commands/IPreCommandExecutionEvent.cs +39 -0
@@ 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;
+
+/// <summary>
+/// Event executed prior to command handler.
+/// </summary>
+public interface IPreCommandExecutionEvent
+{
+ /// <summary>
+ /// Execute the command pre execution event.
+ /// </summary>
+ /// <remarks>
+ /// If an error is returned, the command handler won't be called.
+ /// </remarks>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <typeparam name="TCommand">The type of the command.</typeparam>
+ /// <returns>A result that may or may not succeed.</returns>
+ public Task<Result> ExecuteBeforeCommandAsync<TCommand>
+ (
+ INostaleClient client,
+ TCommand command,
+ CancellationToken ct = default
+ )
+ where TCommand : ICommand;
+}<
\ No newline at end of file
M Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +25 -0
@@ 172,6 172,30 @@ public static class ServiceCollectionExtensions
}
/// <summary>
+ /// Add the given pre execution event that will be executed before the command handler.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TEvent">The pre execution event type.</typeparam>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddPreCommandExecutionEvent<TEvent>(this IServiceCollection serviceCollection)
+ where TEvent : class, IPreCommandExecutionEvent
+ {
+ return serviceCollection.AddScoped<IPreCommandExecutionEvent, TEvent>();
+ }
+
+ /// <summary>
+ /// Add the given post execution event that will be executed after the command handler.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TEvent">The pre execution event type.</typeparam>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddPostCommandExecutionEvent<TEvent>(this IServiceCollection serviceCollection)
+ where TEvent : class, IPostCommandExecutionEvent
+ {
+ return serviceCollection.AddScoped<IPostCommandExecutionEvent, TEvent>();
+ }
+
+ /// <summary>
/// Add the given pre execution event that will be executed before the packet responders.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
@@ 207,6 231,7 @@ public static class ServiceCollectionExtensions
.AddScoped<StatefulInjector>()
.AddSingleton<StatefulRepository>()
.AddPreExecutionEvent<StatefulPreExecutionEvent>()
+ .AddPreCommandExecutionEvent<StatefulPreExecutionEvent>()
.AddScoped<INostaleClient>
(
p =>
M Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs => Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs +10 -1
@@ 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;
/// <summary>
/// Event that injects stateful entities into the scope.
/// </summary>
-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());
}
+
+ /// <inheritdoc />
+ public Task<Result> ExecuteBeforeCommandAsync<TCommand>(INostaleClient client, TCommand command, CancellationToken ct = default)
+ where TCommand : ICommand
+ {
+ _injector.Client = client;
+ return Task.FromResult(Result.FromSuccess());
+ }
}=
\ No newline at end of file