~ruther/NosSmooth

d42ac940be2be4953dec8a9853fc5801c48dff29 — Rutherther 3 years ago 5ee7ece
feat(core): add command pre and post events
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

Do not follow this link