diff --git a/StateR.sln b/StateR.sln index 4dad392..78e4f50 100644 --- a/StateR.sln +++ b/StateR.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CounterApp", "samples\Count EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CounterApp.Tests", "samples\CounterApp\CounterApp.Tests\CounterApp.Tests.csproj", "{FBDEBA94-7F63-4CB5-AC13-4D0874730316}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StateR.Microsoft.Extensions.DependencyInjection", "src\StateR.Microsoft.Extensions.DependencyInjection\StateR.Microsoft.Extensions.DependencyInjection.csproj", "{5C432129-E637-4895-895D-1FDFDC61C049}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -119,6 +121,18 @@ Global {FBDEBA94-7F63-4CB5-AC13-4D0874730316}.Release|x64.Build.0 = Release|Any CPU {FBDEBA94-7F63-4CB5-AC13-4D0874730316}.Release|x86.ActiveCfg = Release|Any CPU {FBDEBA94-7F63-4CB5-AC13-4D0874730316}.Release|x86.Build.0 = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|x64.Build.0 = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Debug|x86.Build.0 = Debug|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|Any CPU.Build.0 = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|x64.ActiveCfg = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|x64.Build.0 = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|x86.ActiveCfg = Release|Any CPU + {5C432129-E637-4895-895D-1FDFDC61C049}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,6 +146,7 @@ Global {E72B8B55-3F7B-4EE2-955B-B5FAB222EBD9} = {22B777AC-AFAE-422A-B4CD-48C907251D9E} {99926EB0-84F9-4906-8F7E-4E1873A403EB} = {E72B8B55-3F7B-4EE2-955B-B5FAB222EBD9} {FBDEBA94-7F63-4CB5-AC13-4D0874730316} = {E72B8B55-3F7B-4EE2-955B-B5FAB222EBD9} + {5C432129-E637-4895-895D-1FDFDC61C049} = {F0F6A2CA-0972-43BD-B777-B5656DFE20C3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6ADD1933-C449-475E-9409-AD333C1C48A0} diff --git a/samples/CounterApp/CounterApp/CounterApp.csproj b/samples/CounterApp/CounterApp/CounterApp.csproj index bb327bf..5317fde 100644 --- a/samples/CounterApp/CounterApp/CounterApp.csproj +++ b/samples/CounterApp/CounterApp/CounterApp.csproj @@ -5,14 +5,23 @@ + + + + + + + + + - + diff --git a/samples/CounterApp/CounterApp/Features/Counter.cs b/samples/CounterApp/CounterApp/Features/Counter.cs index 34f30ca..3532ade 100644 --- a/samples/CounterApp/CounterApp/Features/Counter.cs +++ b/samples/CounterApp/CounterApp/Features/Counter.cs @@ -1,19 +1,13 @@ using FluentValidation; using StateR; -using StateR.AfterEffects; -using StateR.Blazor.Persistance; -using ForEvolve.Blazor.WebStorage; -using StateR.Interceptors; -using StateR.Internal; +//using StateR.Blazor.Persistance; using StateR.Updaters; -using System; -using System.Reflection; namespace CounterApp.Features; public class Counter { - [Persist] + //[Persist] public record class State(int Count) : StateBase; public class InitialState : IInitialState @@ -21,10 +15,10 @@ public class InitialState : IInitialState public State Value => new(0); } - public record class Increment : IAction; - public record class Decrement : IAction; - public record class SetPositive(int Count) : IAction; - public record class SetNegative(int Count) : IAction; + public record class Increment : IAction; + public record class Decrement : IAction; + public record class SetPositive(int Count) : IAction; + public record class SetNegative(int Count) : IAction; public class Updaters : IUpdater, IUpdater, IUpdater, IUpdater { diff --git a/samples/CounterApp/CounterApp/Features/WeatherForecast.cs b/samples/CounterApp/CounterApp/Features/WeatherForecast.cs index c1c294d..e0f1818 100644 --- a/samples/CounterApp/CounterApp/Features/WeatherForecast.cs +++ b/samples/CounterApp/CounterApp/Features/WeatherForecast.cs @@ -1,7 +1,5 @@ using StateR; -using StateR.AfterEffects; using StateR.AsyncLogic; -using StateR.Interceptors; using StateR.Internal; using StateR.Updaters; using System.Collections.Immutable; @@ -37,13 +35,13 @@ public State Update(Reload action, State state) => state with { Status = AsyncOperationStatus.Idle, Forecasts = ImmutableList.Create() }; } - public class ReloadEffect : IAfterEffects - { - public async Task HandleAfterEffectAsync(IDispatchContext context, CancellationToken cancellationToken) - { - await context.Dispatcher.DispatchAsync(new Fetch(), cancellationToken); - } - } + //public class ReloadEffect : IAfterEffects + //{ + // public async Task HandleAfterEffectAsync(IDispatchContext context, CancellationToken cancellationToken) + // { + // await context.Dispatcher.DispatchAsync(new Fetch(), cancellationToken); + // } + //} public class FetchOperation : AsyncOperation { @@ -61,12 +59,12 @@ protected override async Task LoadAsync(Fetch action, State initalState } } - public class Delays : IInterceptor> - { - public async Task InterceptAsync(IDispatchContext> context, CancellationToken cancellationToken) - { - Console.WriteLine($"{context.Action.GetType().GetStatorName()}: {context.Action.status}"); - await Task.Delay(2000, cancellationToken); - } - } + //public class Delays : IInterceptor> + //{ + // public async Task InterceptAsync(IDispatchContext> context, CancellationToken cancellationToken) + // { + // Console.WriteLine($"{context.Action.GetType().GetStatorName()}: {context.Action.status}"); + // await Task.Delay(2000, cancellationToken); + // } + //} } diff --git a/samples/CounterApp/CounterApp/Pages/FetchData.razor b/samples/CounterApp/CounterApp/Pages/FetchData.razor index bfb84d9..f66ae6b 100644 --- a/samples/CounterApp/CounterApp/Pages/FetchData.razor +++ b/samples/CounterApp/CounterApp/Pages/FetchData.razor @@ -1,4 +1,4 @@ -@using StateR.AsyncLogic +@*@using StateR.AsyncLogic @using StateR.Blazor.Components @page "/fetchdata" @inherits StatorComponent @@ -10,14 +10,14 @@

This component demonstrates fetching data from the server.

Async Status: @WeatherState.Current.Status

- +*@ @**@ @**@ - +@*
Loading... @@ -60,3 +60,4 @@
+*@ \ No newline at end of file diff --git a/samples/CounterApp/CounterApp/Program.cs b/samples/CounterApp/CounterApp/Program.cs index 3065212..0742e60 100644 --- a/samples/CounterApp/CounterApp/Program.cs +++ b/samples/CounterApp/CounterApp/Program.cs @@ -1,14 +1,11 @@ using CounterApp; -using CounterApp.Features; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using StateR; -using StateR.AfterEffects; -using StateR.Blazor.Persistance; -using StateR.Blazor.ReduxDevTools; +//using StateR.Blazor.Persistance; +//using StateR.Blazor.ReduxDevTools; using ForEvolve.Blazor.WebStorage; -using StateR.Experiments.AsyncLogic; -using StateR.Interceptors; +//using StateR.Experiments.AsyncLogic; using StateR.Validations.FluentValidation; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -42,12 +39,21 @@ public static void RegisterServices(this IServiceCollection services) { var appAssembly = typeof(App).Assembly; services - .AddStateR(appAssembly) - .AddAsyncOperations() - .AddReduxDevTools() + .AddStateR() + + // TODO: scan for types instead + .AddState() + .AddAction(typeof(CounterApp.Features.Counter.Increment)) + .AddAction(typeof(CounterApp.Features.Counter.Decrement)) + .AddAction(typeof(CounterApp.Features.Counter.SetPositive)) + .AddAction(typeof(CounterApp.Features.Counter.SetNegative)) + .AddUpdaters(typeof(CounterApp.Features.Counter.Updaters)) + + //.AddAsyncOperations() + //.AddReduxDevTools() .AddFluentValidation(appAssembly) .Apply(buidler => buidler - .AddPersistence() + //.AddPersistence() .AddStateValidation() ) ; diff --git a/samples/CounterApp/CounterApp/Shared/FluentValidationSummary.razor b/samples/CounterApp/CounterApp/Shared/FluentValidationSummary.razor index 3c74c88..3bd1c75 100644 --- a/samples/CounterApp/CounterApp/Shared/FluentValidationSummary.razor +++ b/samples/CounterApp/CounterApp/Shared/FluentValidationSummary.razor @@ -1,4 +1,4 @@ -@using FluentValidation.Results +@*@using FluentValidation.Results @using StateR.Validations.FluentValidation; @inherits StatorComponent @inject IState ValidationState @@ -29,3 +29,4 @@ await DispatchAsync(new CleanValidationError()); } } +*@ \ No newline at end of file diff --git a/samples/CounterApp/CounterApp/Shared/MainLayout.razor b/samples/CounterApp/CounterApp/Shared/MainLayout.razor index 8751a37..73eba5d 100644 --- a/samples/CounterApp/CounterApp/Shared/MainLayout.razor +++ b/samples/CounterApp/CounterApp/Shared/MainLayout.razor @@ -1,4 +1,4 @@ -@using StateR.Blazor.Components; +@*@using StateR.Blazor.Components;*@ @inherits LayoutComponentBase
@@ -15,5 +15,5 @@ @Body - + @**@
diff --git a/src/StateR.Blazor.Experiments/ReduxDevTools/ReduxDevToolsInterop.cs b/src/StateR.Blazor.Experiments/ReduxDevTools/ReduxDevToolsInterop.cs index cf9133d..3e98ce1 100644 --- a/src/StateR.Blazor.Experiments/ReduxDevTools/ReduxDevToolsInterop.cs +++ b/src/StateR.Blazor.Experiments/ReduxDevTools/ReduxDevToolsInterop.cs @@ -1,7 +1,6 @@ using Microsoft.JSInterop; using StateR.Internal; using StateR.Updaters; -using StateR.Updaters.Hooks; using System.Collections; using System.Reflection; using System.Text.Json; diff --git a/src/StateR.Blazor/StatorComponentBase.cs b/src/StateR.Blazor/StatorComponentBase.cs index b0bcaa4..f90d387 100644 --- a/src/StateR.Blazor/StatorComponentBase.cs +++ b/src/StateR.Blazor/StatorComponentBase.cs @@ -10,13 +10,21 @@ public abstract class StatorComponentBase : ComponentBase, IDisposable [Inject] public IDispatcher? Dispatcher { get; set; } - protected virtual async Task DispatchAsync(TAction action, CancellationToken cancellationToken = default) - where TAction : IAction + protected virtual async Task DispatchAsync(object action, CancellationToken cancellationToken = default) { GuardAgainstNullDispatcher(); await Dispatcher.DispatchAsync(action, cancellationToken); } + + protected virtual async Task DispatchAsync(TAction action, CancellationToken cancellationToken = default) + where TAction : IAction + where TState : StateBase + { + GuardAgainstNullDispatcher(); + await Dispatcher.DispatchAsync(action, cancellationToken); + } + private void Dispose(bool disposing) { if (!_disposedValue) diff --git a/src/StateR.Experiments/AsyncLogic/AsyncError.cs b/src/StateR.Experiments/AsyncLogic/AsyncError.cs index a160565..5a97a8b 100644 --- a/src/StateR.Experiments/AsyncLogic/AsyncError.cs +++ b/src/StateR.Experiments/AsyncLogic/AsyncError.cs @@ -6,7 +6,7 @@ public class AsyncError { public record State : StateBase { - public IAction? Action { get; init; } + public IAction? Action { get; init; } public AsyncState? InitialState { get; init; } public AsyncState? ActualState { get; init; } public Exception? Exception { get; init; } @@ -21,7 +21,7 @@ public class InitialState : IInitialState public State Value => new(); } - public record Occured(IAction Action, AsyncState InitialState, AsyncState ActualState, Exception Exception) : IAction; + public record Occured(IAction Action, AsyncState InitialState, AsyncState ActualState, Exception Exception) : IAction; public class Updaters : IUpdater { diff --git a/src/StateR.Experiments/AsyncLogic/StartupExtensions.cs b/src/StateR.Experiments/AsyncLogic/StartupExtensions.cs index e7fbd92..9496dda 100644 --- a/src/StateR.Experiments/AsyncLogic/StartupExtensions.cs +++ b/src/StateR.Experiments/AsyncLogic/StartupExtensions.cs @@ -13,7 +13,7 @@ public static IStatorBuilder AddAsyncOperations(this IStatorBuilder builder) builder.AddTypes(new[] { typeof(StatusUpdated<>) }); // Async Operation's Errors - builder.Services.TryAddSingleton, UpdaterActionHandler>(); + builder.Services.TryAddSingleton, UpdaterMiddleware>(); builder.Services.TryAddSingleton, AsyncError.Updaters>(); builder.Services.TryAddSingleton, AsyncError.InitialState>(); builder.Services.TryAddSingleton, Internal.State>(); diff --git a/src/StateR.Experiments/StateR.Experiments.csproj b/src/StateR.Experiments/StateR.Experiments.csproj index 95009eb..0cf5587 100644 --- a/src/StateR.Experiments/StateR.Experiments.csproj +++ b/src/StateR.Experiments/StateR.Experiments.csproj @@ -5,6 +5,24 @@ StateR + + + + + + + + + + + + + + + + + + diff --git a/src/StateR.Experiments/Validations/FluentValidation/StartupExtensions.cs b/src/StateR.Experiments/Validations/FluentValidation/StartupExtensions.cs index 963b5e7..c7994a1 100644 --- a/src/StateR.Experiments/Validations/FluentValidation/StartupExtensions.cs +++ b/src/StateR.Experiments/Validations/FluentValidation/StartupExtensions.cs @@ -1,7 +1,7 @@ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using StateR.Interceptors; +using StateR.Pipeline; using System.Reflection; namespace StateR.Validations.FluentValidation; @@ -13,17 +13,16 @@ public static IStatorBuilder AddFluentValidation(this IStatorBuilder builder, pa ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentNullException.ThrowIfNull(assembliesToScan, nameof(assembliesToScan)); - // Validation action - builder.AddTypes(new[] { - typeof(AddValidationErrors), - typeof(ReplaceValidationErrors), - typeof(ValidationUpdaters), - typeof(ValidationInitialState), - typeof(ValidationState), - }); + // Add state, actions, and updaters + builder + .AddState() + .AddAction(typeof(AddValidationErrors)) + .AddAction(typeof(ReplaceValidationErrors)) + .AddUpdaters(typeof(ValidationUpdaters)) + ; - // Validation interceptor and state - builder.Services.TryAddSingleton(typeof(IInterceptor<>), typeof(ValidationInterceptor<>)); + // Validation interceptor + builder.Services.TryAddSingleton(typeof(IActionFilter<,>), typeof(ValidationFilter<,>)); // Scan for validators builder.Services.AddValidatorsFromAssemblies(assembliesToScan, ServiceLifetime.Singleton); diff --git a/src/StateR.Experiments/Validations/FluentValidation/StateValidationDecorator.cs b/src/StateR.Experiments/Validations/FluentValidation/StateValidationDecorator.cs index 91a8a2b..5769a29 100644 --- a/src/StateR.Experiments/Validations/FluentValidation/StateValidationDecorator.cs +++ b/src/StateR.Experiments/Validations/FluentValidation/StateValidationDecorator.cs @@ -1,10 +1,7 @@ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using StateR.ActionHandlers; using StateR.Internal; -using System; -using System.Reflection; namespace StateR.Validations.FluentValidation; @@ -53,53 +50,54 @@ public static class StateValidatorStartupExtensions { public static IStatorBuilder AddStateValidation(this IStatorBuilder builder) { - RegisterStateDecorator(builder.Services, builder.All); - ActionHandlerDecorator(builder.Services); + //RegisterStateDecorator(builder.Services, builder.All); + //ActionHandlerDecorator(builder.Services); return builder; } - private static void ActionHandlerDecorator(IServiceCollection services) - { - Console.WriteLine("- Decorate, ValidationExceptionActionHandlersManagerDecorator>()"); - services.Decorate(); - } - private static void RegisterStateDecorator(IServiceCollection services, IEnumerable types) - { - var states = TypeScanner.FindStates(types); - Console.WriteLine("StateValidator:"); - foreach (var state in states) - { - Console.WriteLine($"- Decorate, StateValidationDecorator<{state.GetStatorName()}>>()"); + //private static void ActionHandlerDecorator(IServiceCollection services) + //{ + // Console.WriteLine("- Decorate, ValidationExceptionActionHandlersManagerDecorator>()"); + // services.Decorate(); + //} - // Equivalent to: Decorate, StateValidationDecorator>(); - var stateType = typeof(IState<>).MakeGenericType(state); - var stateSessionDecoratorType = typeof(StateValidationDecorator<>).MakeGenericType(state); - services.Decorate(stateType, stateSessionDecoratorType); - } - } + //private static void RegisterStateDecorator(IServiceCollection services, IEnumerable types) + //{ + // var states = TypeScanner.FindStates(types); + // Console.WriteLine("StateValidator:"); + // foreach (var state in states) + // { + // Console.WriteLine($"- Decorate, StateValidationDecorator<{state.GetStatorName()}>>()"); + + // // Equivalent to: Decorate, StateValidationDecorator>(); + // var stateType = typeof(IState<>).MakeGenericType(state); + // var stateSessionDecoratorType = typeof(StateValidationDecorator<>).MakeGenericType(state); + // services.Decorate(stateType, stateSessionDecoratorType); + // } + //} } -public class ValidationExceptionActionHandlersManagerDecorator : IActionHandlersManager -{ - private readonly IActionHandlersManager _next; - public ValidationExceptionActionHandlersManagerDecorator(IActionHandlersManager next) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } +//public class ValidationExceptionActionHandlersManagerDecorator : IActionFilter +//{ +// private readonly IActionHandlersManager _next; +// public ValidationExceptionActionHandlersManagerDecorator(IActionHandlersManager next) +// { +// _next = next ?? throw new ArgumentNullException(nameof(next)); +// } - public async Task DispatchAsync(IDispatchContext dispatchContext) - where TAction : IAction - { - try - { - await _next.DispatchAsync(dispatchContext); - } - catch (ValidationException ex) - { - await dispatchContext.Dispatcher.DispatchAsync( - new AddValidationErrors(ex.Errors), - dispatchContext.CancellationToken - ); - } - } -} +// public async Task DispatchAsync(IDispatchContext dispatchContext) +// where TAction : IAction +// { +// try +// { +// await _next.DispatchAsync(dispatchContext); +// } +// catch (ValidationException ex) +// { +// await dispatchContext.Dispatcher.DispatchAsync( +// new AddValidationErrors(ex.Errors), +// dispatchContext.CancellationToken +// ); +// } +// } +//} diff --git a/src/StateR.Experiments/Validations/FluentValidation/ValidationInterceptor.cs b/src/StateR.Experiments/Validations/FluentValidation/ValidationFilter.cs similarity index 68% rename from src/StateR.Experiments/Validations/FluentValidation/ValidationInterceptor.cs rename to src/StateR.Experiments/Validations/FluentValidation/ValidationFilter.cs index 9a140b9..fa14a1c 100644 --- a/src/StateR.Experiments/Validations/FluentValidation/ValidationInterceptor.cs +++ b/src/StateR.Experiments/Validations/FluentValidation/ValidationFilter.cs @@ -1,22 +1,25 @@ using FluentValidation; using FluentValidation.Results; -using StateR.Interceptors; +using StateR.Pipeline; using StateR.Updaters; using System.Collections.Immutable; namespace StateR.Validations.FluentValidation; -public class ValidationInterceptor : IInterceptor - where TAction : IAction +public class ValidationFilter : IActionFilter + where TAction : IAction + where TState : StateBase { private readonly IEnumerable> _validators; - public ValidationInterceptor(IEnumerable> validators) + public ValidationFilter(IEnumerable> validators) { _validators = validators; } - public async Task InterceptAsync(IDispatchContext context, CancellationToken cancellationToken) + public async Task InvokeAsync(IDispatchContext context, ActionDelegate? next, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(next); + var result = _validators .Select(validator => validator.Validate(context.Action)); if (result?.Any(validator => !validator.IsValid) ?? false) @@ -27,6 +30,18 @@ public async Task InterceptAsync(IDispatchContext context, Cancellation await context.Dispatcher.DispatchAsync(new AddValidationErrors(errors), cancellationToken); context.Cancel(); } + try + { + await next(context, cancellationToken); + } + catch (ValidationException ex) + { + Console.WriteLine(ex.Message); + await context.Dispatcher.DispatchAsync( + new AddValidationErrors(ex.Errors), + context.CancellationToken + ); + } } } @@ -39,10 +54,10 @@ public class ValidationInitialState : IInitialState public ValidationState Value => new(ImmutableList.Create()); } -public record class AddValidationErrors(IEnumerable Errors) : IAction; -public record class ReplaceValidationErrors(IEnumerable Errors) : IAction; -public record class CleanValidationError() : IAction; -public record class RemoveValidationError(ValidationFailure Error) : IAction; +public record class AddValidationErrors(IEnumerable Errors) : IAction; +public record class ReplaceValidationErrors(IEnumerable Errors) : IAction; +public record class CleanValidationError() : IAction; +public record class RemoveValidationError(ValidationFailure Error) : IAction; public class ValidationUpdaters : IUpdater, diff --git a/src/StateR.Microsoft.Extensions.DependencyInjection/Internal/TypeScanner.cs b/src/StateR.Microsoft.Extensions.DependencyInjection/Internal/TypeScanner.cs new file mode 100644 index 0000000..7a0801a --- /dev/null +++ b/src/StateR.Microsoft.Extensions.DependencyInjection/Internal/TypeScanner.cs @@ -0,0 +1,59 @@ +using StateR.Pipeline; +using StateR.Updaters; +using System.Reflection; + +namespace StateR.Internal; + +public static class TypeScannerExtensions +{ + public static IEnumerable FindStates(this IEnumerable types) + { + var states = types + .Where(type => !type.IsAbstract && type.IsSubclassOf(typeof(StateBase))); + return states; + } + + public static IEnumerable FindInitialStates(this IEnumerable types) + { + var initialStates = types + .Where(type => !type.IsAbstract && type + .GetTypeInfo() + .GetInterfaces() + .Any(i => i == typeof(IInitialState<>)) + ); + return initialStates; + } + + public static IEnumerable FindActions(this IEnumerable types) + { + var actions = types + .Where(type => !type.IsAbstract && type + .GetTypeInfo() + .GetInterfaces() + .Any(i => i == typeof(IAction<>)) + ); + return actions; + } + + public static IEnumerable FindUpdaters(this IEnumerable types) + { + var updaters = types + .Where(type => !type.IsAbstract && type + .GetTypeInfo() + .GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IUpdater<,>)) + ); + return updaters; + } + + public static IEnumerable FindActionFilters(this IEnumerable types) + { + var handlers = types + .Where(type => !type.IsAbstract && type + .GetTypeInfo() + .GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IActionFilter<,>)) + ); + return handlers; + } +} \ No newline at end of file diff --git a/src/StateR.Microsoft.Extensions.DependencyInjection/StateR.Microsoft.Extensions.DependencyInjection.csproj b/src/StateR.Microsoft.Extensions.DependencyInjection/StateR.Microsoft.Extensions.DependencyInjection.csproj new file mode 100644 index 0000000..b289158 --- /dev/null +++ b/src/StateR.Microsoft.Extensions.DependencyInjection/StateR.Microsoft.Extensions.DependencyInjection.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + StateR + + + + + + + + + + diff --git a/src/StateR.Microsoft.Extensions.DependencyInjection/StatorStartupExtensions.cs b/src/StateR.Microsoft.Extensions.DependencyInjection/StatorStartupExtensions.cs new file mode 100644 index 0000000..03a8b60 --- /dev/null +++ b/src/StateR.Microsoft.Extensions.DependencyInjection/StatorStartupExtensions.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StateR.Internal; +using StateR.Pipeline; +using StateR.Updaters; +using System.Reflection; + +namespace StateR; + +public static class StatorStartupExtensions +{ + public static IStatorBuilder AddStateR(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return new StatorBuilder(services); + } + + public static IStatorBuilder ScanAndAddStates(this IStatorBuilder builder, params Assembly[] assembliesToScan) + { + var initialStates = assembliesToScan + .SelectMany(a => a.GetTypes()) + .FindInitialStates() + ; + + foreach (var initialState in initialStates) + { + var state = initialState.GenericTypeArguments[0]; + builder.AddState(state, initialState); + } + + return builder; + } + + public static IServiceCollection Apply(this IStatorBuilder builder, Action? postConfiguration = null) + { + // Register States + foreach (var state in builder.States) + { + Console.WriteLine($"state: {state.FullName}"); + + // Equivalent to: AddSingleton, State>(); + var stateServiceType = typeof(IState<>).MakeGenericType(state); + var stateImplementationType = typeof(State<>).MakeGenericType(state); + builder.Services.AddSingleton(stateServiceType, stateImplementationType); + } + + // Register Initial States + builder.Services.Scan(s => s + .AddTypes(builder.InitialStates) + + // Equivalent to: AddSingleton, Implementation>(); + .AddClasses(classes => classes.AssignableTo(typeof(IInitialState<>))) + .AsImplementedInterfaces() + .WithSingletonLifetime() + ); + + // Register Updaters and their respective IActionFilter + var iUpdaterType = typeof(IUpdater<,>); + var updaterHandler = typeof(UpdaterMiddleware<,>); + var handlerType = typeof(IActionFilter<,>); + foreach (var updater in builder.Updaters) + { + Console.WriteLine($"updater: {updater.FullName}"); + var interfaces = updater.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == iUpdaterType); + foreach (var @interface in interfaces) + { + // Equivalent to: AddSingleton, UpdaterMiddleware> + var actionType = @interface.GenericTypeArguments[0]; + var stateType = @interface.GenericTypeArguments[1]; + var iMiddlewareServiceType = handlerType.MakeGenericType(actionType, stateType); + var updaterMiddlewareImplementationType = updaterHandler.MakeGenericType(stateType, actionType); + builder.Services.AddSingleton(iMiddlewareServiceType, updaterMiddlewareImplementationType); + + // Equivalent to: AddSingleton, Updater>(); + builder.Services.AddSingleton(@interface, updater); + + Console.WriteLine($"- AddSingleton<{iMiddlewareServiceType.GetStatorName()}, {updaterMiddlewareImplementationType.GetStatorName()}>()"); + Console.WriteLine($"- AddSingleton<{@interface.GetStatorName()}, {updater.GetStatorName()}>()"); + } + } + + var iActionFilterType = typeof(IActionFilter<,>); + foreach (var filter in builder.ActionFilters) + { + var interfaces = filter.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == iActionFilterType); + foreach (var @interface in interfaces) + { + var actionType = @interface.GenericTypeArguments[0]; + var stateType = @interface.GenericTypeArguments[1]; + var filterType = iActionFilterType.MakeGenericType(actionType, stateType); + + builder.Services.AddSingleton(@interface, filter); + } + } + + return builder.Services; + } +} diff --git a/src/StateR/ActionHandlers/ActionHandlersManager.cs b/src/StateR/ActionHandlers/ActionHandlersManager.cs deleted file mode 100644 index df9c673..0000000 --- a/src/StateR/ActionHandlers/ActionHandlersManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using StateR.ActionHandlers.Hooks; - -namespace StateR.ActionHandlers; - -public class ActionHandlersManager : IActionHandlersManager -{ - private readonly IActionHandlerHooksCollection _hooksCollection; - private readonly IServiceProvider _serviceProvider; - - public ActionHandlersManager(IActionHandlerHooksCollection hooksCollection, IServiceProvider serviceProvider) - { - _hooksCollection = hooksCollection ?? throw new ArgumentNullException(nameof(hooksCollection)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } - - public async Task DispatchAsync(IDispatchContext dispatchContext) where TAction : IAction - { - var updaterHandlers = _serviceProvider.GetServices>().ToList(); - foreach (var handler in updaterHandlers) - { - dispatchContext.CancellationToken.ThrowIfCancellationRequested(); - - await _hooksCollection.BeforeHandlerAsync(dispatchContext, handler, dispatchContext.CancellationToken); - await handler.HandleAsync(dispatchContext, dispatchContext.CancellationToken); - await _hooksCollection.AfterHandlerAsync(dispatchContext, handler, dispatchContext.CancellationToken); - } - } -} diff --git a/src/StateR/ActionHandlers/Hooks/ActionHandlerHooksCollection.cs b/src/StateR/ActionHandlers/Hooks/ActionHandlerHooksCollection.cs deleted file mode 100644 index f2d8eab..0000000 --- a/src/StateR/ActionHandlers/Hooks/ActionHandlerHooksCollection.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace StateR.ActionHandlers.Hooks; - -public class ActionHandlerHooksCollection : IActionHandlerHooksCollection -{ - private readonly IEnumerable _beforeActionHooks; - private readonly IEnumerable _afterActionHooks; - public ActionHandlerHooksCollection(IEnumerable beforeActionHooks, IEnumerable afterActionHooks) - { - _beforeActionHooks = beforeActionHooks ?? throw new ArgumentNullException(nameof(beforeActionHooks)); - _afterActionHooks = afterActionHooks ?? throw new ArgumentNullException(nameof(afterActionHooks)); - } - - public async Task BeforeHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _beforeActionHooks) - { - await hook.BeforeHandlerAsync(context, actionHandler, cancellationToken); - } - } - - public async Task AfterHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _afterActionHooks) - { - await hook.AfterHandlerAsync(context, actionHandler, cancellationToken); - } - } -} diff --git a/src/StateR/ActionHandlers/Hooks/IActionHandlerHooksCollection.cs b/src/StateR/ActionHandlers/Hooks/IActionHandlerHooksCollection.cs deleted file mode 100644 index 2964422..0000000 --- a/src/StateR/ActionHandlers/Hooks/IActionHandlerHooksCollection.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.ActionHandlers.Hooks; - -public interface IActionHandlerHooksCollection -{ - Task BeforeHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction; - Task AfterHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/ActionHandlers/Hooks/IAfterActionHook.cs b/src/StateR/ActionHandlers/Hooks/IAfterActionHook.cs deleted file mode 100644 index b1ecd82..0000000 --- a/src/StateR/ActionHandlers/Hooks/IAfterActionHook.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StateR.ActionHandlers.Hooks; - -public interface IAfterActionHook -{ - Task AfterHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/ActionHandlers/Hooks/IBeforeActionHook.cs b/src/StateR/ActionHandlers/Hooks/IBeforeActionHook.cs deleted file mode 100644 index c69f7c8..0000000 --- a/src/StateR/ActionHandlers/Hooks/IBeforeActionHook.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StateR.ActionHandlers.Hooks; - -public interface IBeforeActionHook -{ - Task BeforeHandlerAsync(IDispatchContext context, IActionHandler actionHandler, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/ActionHandlers/IActionHandler.cs b/src/StateR/ActionHandlers/IActionHandler.cs deleted file mode 100644 index 4f7747f..0000000 --- a/src/StateR/ActionHandlers/IActionHandler.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.ActionHandlers; - -public interface IActionHandler - where TAction : IAction -{ - Task HandleAsync(IDispatchContext context, CancellationToken cancellationToken); -} diff --git a/src/StateR/ActionHandlers/IActionHandlersManager.cs b/src/StateR/ActionHandlers/IActionHandlersManager.cs deleted file mode 100644 index aa0a671..0000000 --- a/src/StateR/ActionHandlers/IActionHandlersManager.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace StateR.ActionHandlers; - -public interface IActionHandlersManager : IDispatchManager { } diff --git a/src/StateR/AfterEffects/AfterEffectsManager.cs b/src/StateR/AfterEffects/AfterEffectsManager.cs deleted file mode 100644 index 72cdb58..0000000 --- a/src/StateR/AfterEffects/AfterEffectsManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using StateR.AfterEffects.Hooks; - -namespace StateR.AfterEffects; - -public class AfterEffectsManager : IAfterEffectsManager -{ - private readonly IAfterEffectHooksCollection _hooks; - private readonly IServiceProvider _serviceProvider; - - public AfterEffectsManager(IAfterEffectHooksCollection hooks, IServiceProvider serviceProvider) - { - _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } - - public async Task DispatchAsync(IDispatchContext dispatchContext) where TAction : IAction - { - var afterEffects = _serviceProvider.GetServices>().ToList(); - foreach (var afterEffect in afterEffects) - { - dispatchContext.CancellationToken.ThrowIfCancellationRequested(); - - await _hooks.BeforeHandlerAsync(dispatchContext, afterEffect, dispatchContext.CancellationToken); - await afterEffect.HandleAfterEffectAsync(dispatchContext, dispatchContext.CancellationToken); - await _hooks.AfterHandlerAsync(dispatchContext, afterEffect, dispatchContext.CancellationToken); - } - } -} diff --git a/src/StateR/AfterEffects/Hooks/AfterEffectHooksCollection.cs b/src/StateR/AfterEffects/Hooks/AfterEffectHooksCollection.cs deleted file mode 100644 index 1e6a603..0000000 --- a/src/StateR/AfterEffects/Hooks/AfterEffectHooksCollection.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace StateR.AfterEffects.Hooks; - -public class AfterEffectHooksCollection : IAfterEffectHooksCollection -{ - private readonly IEnumerable _beforeAfterEffectHooks; - private readonly IEnumerable _afterAfterEffectHooks; - public AfterEffectHooksCollection(IEnumerable beforeAfterEffectHooks, IEnumerable afterAfterEffectHooks) - { - _beforeAfterEffectHooks = beforeAfterEffectHooks ?? throw new ArgumentNullException(nameof(beforeAfterEffectHooks)); - _afterAfterEffectHooks = afterAfterEffectHooks ?? throw new ArgumentNullException(nameof(afterAfterEffectHooks)); - } - - public async Task BeforeHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _beforeAfterEffectHooks) - { - await hook.BeforeHandlerAsync(context, afterEffect, cancellationToken); - } - } - - public async Task AfterHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _afterAfterEffectHooks) - { - await hook.AfterHandlerAsync(context, afterEffect, cancellationToken); - } - } -} - diff --git a/src/StateR/AfterEffects/Hooks/IAfterAfterEffectHook.cs b/src/StateR/AfterEffects/Hooks/IAfterAfterEffectHook.cs deleted file mode 100644 index 8b8d888..0000000 --- a/src/StateR/AfterEffects/Hooks/IAfterAfterEffectHook.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.AfterEffects.Hooks; - -public interface IAfterAfterEffectHook -{ - Task AfterHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction; -} - diff --git a/src/StateR/AfterEffects/Hooks/IAfterEffectHooksCollection.cs b/src/StateR/AfterEffects/Hooks/IAfterEffectHooksCollection.cs deleted file mode 100644 index d0e4ce9..0000000 --- a/src/StateR/AfterEffects/Hooks/IAfterEffectHooksCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StateR.AfterEffects.Hooks; - -public interface IAfterEffectHooksCollection -{ - Task BeforeHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction; - Task AfterHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction; -} - diff --git a/src/StateR/AfterEffects/Hooks/IBeforeAfterEffectHook.cs b/src/StateR/AfterEffects/Hooks/IBeforeAfterEffectHook.cs deleted file mode 100644 index 7a473b7..0000000 --- a/src/StateR/AfterEffects/Hooks/IBeforeAfterEffectHook.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.AfterEffects.Hooks; - -public interface IBeforeAfterEffectHook -{ - Task BeforeHandlerAsync(IDispatchContext context, IAfterEffects afterEffect, CancellationToken cancellationToken) where TAction : IAction; -} - diff --git a/src/StateR/AfterEffects/IAfterEffects.cs b/src/StateR/AfterEffects/IAfterEffects.cs deleted file mode 100644 index b8761a2..0000000 --- a/src/StateR/AfterEffects/IAfterEffects.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.AfterEffects; - -public interface IAfterEffects - where TAction : IAction -{ - Task HandleAfterEffectAsync(IDispatchContext context, CancellationToken cancellationToken); -} diff --git a/src/StateR/AfterEffects/IAfterEffectsManager.cs b/src/StateR/AfterEffects/IAfterEffectsManager.cs deleted file mode 100644 index 20aa657..0000000 --- a/src/StateR/AfterEffects/IAfterEffectsManager.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace StateR.AfterEffects; - -public interface IAfterEffectsManager : IDispatchManager { } diff --git a/src/StateR/DispatchContext.cs b/src/StateR/DispatchContext.cs index 8f7ea55..085ca0b 100644 --- a/src/StateR/DispatchContext.cs +++ b/src/StateR/DispatchContext.cs @@ -1,7 +1,8 @@ namespace StateR; -public class DispatchContext : IDispatchContext - where TAction : IAction +public class DispatchContext : IDispatchContext + where TAction : IAction + where TState : StateBase { private readonly CancellationTokenSource _cancellationTokenSource; public DispatchContext(TAction action, IDispatcher dispatcher, CancellationTokenSource cancellationTokenSource) @@ -16,12 +17,11 @@ public DispatchContext(TAction action, IDispatcher dispatcher, CancellationToken public CancellationToken CancellationToken => _cancellationTokenSource.Token; public void Cancel() - => throw new DispatchCancelledException(Action); - //=> _cancellationTokenSource.Cancel(true); + => throw new DispatchCancelledException(Action.GetType()); } public class DispatchCancelledException : Exception { - public DispatchCancelledException(IAction action) - : base($"The dispatch operation '{action.GetType().FullName}' has been cancelled.") { } + public DispatchCancelledException(Type actionType) + : base($"The dispatch operation '{actionType.FullName}' has been cancelled.") { } } \ No newline at end of file diff --git a/src/StateR/DispatchContextFactory.cs b/src/StateR/DispatchContextFactory.cs index 4238ffc..9ba0810 100644 --- a/src/StateR/DispatchContextFactory.cs +++ b/src/StateR/DispatchContextFactory.cs @@ -2,7 +2,8 @@ public class DispatchContextFactory : IDispatchContextFactory { - public IDispatchContext Create(TAction action, IDispatcher dispatcher, CancellationTokenSource cancellationTokenSource) - where TAction : IAction - => new DispatchContext(action, dispatcher, cancellationTokenSource); + public IDispatchContext Create(TAction action, IDispatcher dispatcher, CancellationTokenSource cancellationTokenSource) + where TAction : IAction + where TState : StateBase + => new DispatchContext(action, dispatcher, cancellationTokenSource); } diff --git a/src/StateR/Dispatcher.cs b/src/StateR/Dispatcher.cs index 607c2fb..2d3836b 100644 --- a/src/StateR/Dispatcher.cs +++ b/src/StateR/Dispatcher.cs @@ -1,44 +1,60 @@ using Microsoft.Extensions.Logging; -using StateR.ActionHandlers; -using StateR.AfterEffects; -using StateR.Interceptors; +using StateR.Pipeline; using System; namespace StateR; public class Dispatcher : IDispatcher { - private readonly IInterceptorsManager _interceptorsManager; - private readonly IActionHandlersManager _actionHandlersManager; - private readonly IAfterEffectsManager _afterEffectsManager; private readonly IDispatchContextFactory _dispatchContextFactory; + private readonly IPipelineFactory _pipelineFactory; private readonly ILogger _logger; - public Dispatcher(IDispatchContextFactory dispatchContextFactory, IInterceptorsManager interceptorsManager, IActionHandlersManager actionHandlersManager, IAfterEffectsManager afterEffectsManager, ILogger logger) + public Dispatcher(IDispatchContextFactory dispatchContextFactory, IPipelineFactory actionFilterFactory, ILogger logger) { _dispatchContextFactory = dispatchContextFactory ?? throw new ArgumentNullException(nameof(dispatchContextFactory)); - _interceptorsManager = interceptorsManager ?? throw new ArgumentNullException(nameof(interceptorsManager)); - _actionHandlersManager = actionHandlersManager ?? throw new ArgumentNullException(nameof(actionHandlersManager)); - _afterEffectsManager = afterEffectsManager ?? throw new ArgumentNullException(nameof(afterEffectsManager)); + _pipelineFactory = actionFilterFactory ?? throw new ArgumentNullException(nameof(actionFilterFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task DispatchAsync(TAction action, CancellationToken cancellationToken) where TAction : IAction + public async Task DispatchAsync(TAction action, CancellationToken cancellationToken) + where TAction : IAction + where TState : StateBase { using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var dispatchContext = _dispatchContextFactory.Create(action, this, cancellationTokenSource); - // - // TODO: design how to handle OperationCanceledException - // + var dispatchContext = _dispatchContextFactory.Create(action, this, cancellationTokenSource); + var pipeline = _pipelineFactory.Create(dispatchContext); try { - await _interceptorsManager.DispatchAsync(dispatchContext); - await _actionHandlersManager.DispatchAsync(dispatchContext); - await _afterEffectsManager.DispatchAsync(dispatchContext); + await pipeline.Invoke(dispatchContext, cancellationToken).ConfigureAwait(false); } catch (DispatchCancelledException ex) { _logger.LogWarning(ex, ex.Message); } } + + public Task DispatchAsync(object action, CancellationToken cancellationToken) + { + var actionType = action + .GetType(); + var actionInterface = actionType.GetInterfaces() + .FirstOrDefault(@interface => @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IAction<>)); + if (actionInterface == null) + { + // TODO: Find a better exception + throw new InvalidOperationException($"The action must implement the {typeof(IAction<>).Name} interface."); + } + var stateType = actionInterface.GetGenericArguments()[0]; + var method = GetType() + .GetMethods() + .FirstOrDefault(m => m.IsGenericMethod && m.Name == nameof(DispatchAsync)); + if(method == null) + { + throw new MissingMethodException(nameof(Dispatcher), nameof(DispatchAsync)); + } + var genericMethod = method.MakeGenericMethod(actionType, stateType); + var task = genericMethod.Invoke(this, new[] { action, cancellationToken }); + return (Task)task!; + } } diff --git a/src/StateR/IAction.cs b/src/StateR/IAction.cs index 648e6b5..c4138a9 100644 --- a/src/StateR/IAction.cs +++ b/src/StateR/IAction.cs @@ -1,3 +1,3 @@ namespace StateR; -public interface IAction { } +public interface IAction where TState : StateBase { } diff --git a/src/StateR/IDispatchContext.cs b/src/StateR/IDispatchContext.cs index 0f96e31..bfe5295 100644 --- a/src/StateR/IDispatchContext.cs +++ b/src/StateR/IDispatchContext.cs @@ -1,7 +1,8 @@ namespace StateR; -public interface IDispatchContext - where TAction : IAction +public interface IDispatchContext + where TAction : IAction + where TState : StateBase { IDispatcher Dispatcher { get; } TAction Action { get; } diff --git a/src/StateR/IDispatchContextFactory.cs b/src/StateR/IDispatchContextFactory.cs index 6613960..1abb378 100644 --- a/src/StateR/IDispatchContextFactory.cs +++ b/src/StateR/IDispatchContextFactory.cs @@ -2,5 +2,7 @@ public interface IDispatchContextFactory { - IDispatchContext Create(TAction action, IDispatcher dispatcher, CancellationTokenSource cancellationTokenSource) where TAction : IAction; + IDispatchContext Create(TAction action, IDispatcher dispatcher, CancellationTokenSource cancellationTokenSource) + where TAction : IAction + where TState : StateBase; } diff --git a/src/StateR/IDispatchManager.cs b/src/StateR/IDispatchManager.cs deleted file mode 100644 index 3cdc889..0000000 --- a/src/StateR/IDispatchManager.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR; - -public interface IDispatchManager -{ - Task DispatchAsync(IDispatchContext dispatchContext) - where TAction : IAction; -} diff --git a/src/StateR/IDispatcher.cs b/src/StateR/IDispatcher.cs index 6ef0bf0..510ccf1 100644 --- a/src/StateR/IDispatcher.cs +++ b/src/StateR/IDispatcher.cs @@ -2,5 +2,8 @@ public interface IDispatcher { - Task DispatchAsync(TAction action, CancellationToken cancellationToken) where TAction : IAction; + Task DispatchAsync(object action, CancellationToken cancellationToken); + Task DispatchAsync(TAction action, CancellationToken cancellationToken) + where TAction : IAction + where TState : StateBase; } diff --git a/src/StateR/IStatorBuilder.cs b/src/StateR/IStatorBuilder.cs index 2a2bd3d..25beaf9 100644 --- a/src/StateR/IStatorBuilder.cs +++ b/src/StateR/IStatorBuilder.cs @@ -1,21 +1,23 @@ using Microsoft.Extensions.DependencyInjection; +using System.Collections.ObjectModel; namespace StateR; public interface IStatorBuilder { IServiceCollection Services { get; } - List Actions { get; } - List States { get; } - //List Interceptors { get; } - List ActionHandlers { get; } - //List AfterEffects { get; } - List Updaters { get; } - List All { get; } + ReadOnlyCollection States { get; } + ReadOnlyCollection InitialStates { get; } + ReadOnlyCollection Actions { get; } + ReadOnlyCollection Updaters { get; } + ReadOnlyCollection ActionFilters { get; } - IStatorBuilder AddTypes(IEnumerable types); - IStatorBuilder AddStates(IEnumerable states); - IStatorBuilder AddActions(IEnumerable states); - IStatorBuilder AddUpdaters(IEnumerable states); - IStatorBuilder AddActionHandlers(IEnumerable types); + IStatorBuilder AddState() + where TState : StateBase + where TInitialState : IInitialState; + IStatorBuilder AddState(Type state, Type initialState); + + IStatorBuilder AddAction(Type actionType); + IStatorBuilder AddUpdaters(Type updaterType); + IStatorBuilder AddActionFilter(Type actionFilterType); } diff --git a/src/StateR/Interceptors/Hooks/IAfterInterceptorHook.cs b/src/StateR/Interceptors/Hooks/IAfterInterceptorHook.cs deleted file mode 100644 index d57c3b8..0000000 --- a/src/StateR/Interceptors/Hooks/IAfterInterceptorHook.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StateR.Interceptors.Hooks; - -public interface IAfterInterceptorHook -{ - Task AfterHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/Interceptors/Hooks/IBeforeInterceptorHook.cs b/src/StateR/Interceptors/Hooks/IBeforeInterceptorHook.cs deleted file mode 100644 index 218ab38..0000000 --- a/src/StateR/Interceptors/Hooks/IBeforeInterceptorHook.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StateR.Interceptors.Hooks; - -public interface IBeforeInterceptorHook -{ - Task BeforeHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/Interceptors/Hooks/IInterceptorsHooksCollection.cs b/src/StateR/Interceptors/Hooks/IInterceptorsHooksCollection.cs deleted file mode 100644 index fa4b8f3..0000000 --- a/src/StateR/Interceptors/Hooks/IInterceptorsHooksCollection.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.Interceptors.Hooks; - -public interface IInterceptorsHooksCollection -{ - Task BeforeHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction; - Task AfterHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction; -} diff --git a/src/StateR/Interceptors/Hooks/InterceptorsHooksCollection.cs b/src/StateR/Interceptors/Hooks/InterceptorsHooksCollection.cs deleted file mode 100644 index ed254bc..0000000 --- a/src/StateR/Interceptors/Hooks/InterceptorsHooksCollection.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace StateR.Interceptors.Hooks; - -public class InterceptorsHooksCollection : IInterceptorsHooksCollection -{ - private readonly IEnumerable _beforeInterceptorHooks; - private readonly IEnumerable _afterInterceptorHooks; - public InterceptorsHooksCollection(IEnumerable beforeInterceptorHooks, IEnumerable afterInterceptorHooks) - { - _beforeInterceptorHooks = beforeInterceptorHooks ?? throw new ArgumentNullException(nameof(beforeInterceptorHooks)); - _afterInterceptorHooks = afterInterceptorHooks ?? throw new ArgumentNullException(nameof(afterInterceptorHooks)); - } - - public async Task BeforeHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _beforeInterceptorHooks) - { - await hook.BeforeHandlerAsync(context, interceptor, cancellationToken); - } - } - - public async Task AfterHandlerAsync(IDispatchContext context, IInterceptor interceptor, CancellationToken cancellationToken) where TAction : IAction - { - foreach (var hook in _afterInterceptorHooks) - { - await hook.AfterHandlerAsync(context, interceptor, cancellationToken); - } - } -} diff --git a/src/StateR/Interceptors/IInterceptor.cs b/src/StateR/Interceptors/IInterceptor.cs deleted file mode 100644 index 0ca7b9d..0000000 --- a/src/StateR/Interceptors/IInterceptor.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StateR.Interceptors; - -public interface IInterceptor - where TAction : IAction -{ - Task InterceptAsync(IDispatchContext context, CancellationToken cancellationToken); -} diff --git a/src/StateR/Interceptors/IInterceptorsManager.cs b/src/StateR/Interceptors/IInterceptorsManager.cs deleted file mode 100644 index 23f6853..0000000 --- a/src/StateR/Interceptors/IInterceptorsManager.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace StateR.Interceptors; - -public interface IInterceptorsManager : IDispatchManager { } diff --git a/src/StateR/Interceptors/InterceptorsManager.cs b/src/StateR/Interceptors/InterceptorsManager.cs deleted file mode 100644 index d23c6ea..0000000 --- a/src/StateR/Interceptors/InterceptorsManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using StateR.Interceptors.Hooks; - -namespace StateR.Interceptors; - -public class InterceptorsManager : IInterceptorsManager -{ - private readonly IInterceptorsHooksCollection _hooks; - private readonly IServiceProvider _serviceProvider; - - public InterceptorsManager(IInterceptorsHooksCollection hooks, IServiceProvider serviceProvider) - { - _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } - - public async Task DispatchAsync(IDispatchContext dispatchContext) where TAction : IAction - { - var interceptors = _serviceProvider.GetServices>().ToList(); - foreach (var interceptor in interceptors) - { - dispatchContext.CancellationToken.ThrowIfCancellationRequested(); - - await _hooks.BeforeHandlerAsync(dispatchContext, interceptor, dispatchContext.CancellationToken); - await interceptor.InterceptAsync(dispatchContext, dispatchContext.CancellationToken); - await _hooks.AfterHandlerAsync(dispatchContext, interceptor, dispatchContext.CancellationToken); - } - } -} diff --git a/src/StateR/Internal/StatorBuilder.cs b/src/StateR/Internal/StatorBuilder.cs index d83df9e..22585e8 100644 --- a/src/StateR/Internal/StatorBuilder.cs +++ b/src/StateR/Internal/StatorBuilder.cs @@ -1,38 +1,183 @@ using Microsoft.Extensions.DependencyInjection; +using StateR.Pipeline; +using StateR.Updaters; +using System.Collections.ObjectModel; namespace StateR.Internal; public class StatorBuilder : IStatorBuilder { + private readonly List _states = new(); + private readonly List _initialStates = new(); + private readonly List _actions = new(); + private readonly List _updaters = new(); + private readonly List _actionFilters = new(); + public StatorBuilder(IServiceCollection services) { Services = services ?? throw new ArgumentNullException(nameof(services)); } + + #region IOldStatorBuilder + public IStatorBuilder AddTypes(IEnumerable types) => AddDistinctTypes(All, types); public IStatorBuilder AddStates(IEnumerable types) - => AddDistinctTypes(States, types); + => AddDistinctTypes(_states, types); public IStatorBuilder AddActions(IEnumerable types) - => AddDistinctTypes(Actions, types); + => AddDistinctTypes(_actions, types); public IStatorBuilder AddUpdaters(IEnumerable types) - => AddDistinctTypes(Updaters, types); + => AddDistinctTypes(_updaters, types); public IStatorBuilder AddActionHandlers(IEnumerable types) => AddDistinctTypes(ActionHandlers, types); public IServiceCollection Services { get; } - public List Actions { get; } = new List(); - public List States { get; } = new List(); public List Interceptors { get; } = new List(); public List ActionHandlers { get; } = new List(); public List AfterEffects { get; } = new List(); - public List Updaters { get; } = new List(); public List All { get; } = new List(); + public IStatorBuilder AddMiddlewares(IEnumerable types) + => AddDistinctTypes(Middlewares, types); + public List Middlewares { get; } = new List(); + private IStatorBuilder AddDistinctTypes(List list, IEnumerable types) { var distinctTypes = types.Except(list).Distinct(); list.AddRange(distinctTypes); return this; } + + #endregion + + public ReadOnlyCollection States => new(_states); + public ReadOnlyCollection InitialStates => new(_initialStates); + public ReadOnlyCollection Actions => new(_actions); + public ReadOnlyCollection Updaters => new(_updaters); + public ReadOnlyCollection ActionFilters => new(_actionFilters); + + public IStatorBuilder AddState() + where TState : StateBase + where TInitialState : IInitialState + { + _states.Add(typeof(TState)); + _initialStates.Add(typeof(TInitialState)); + return this; + } + + public IStatorBuilder AddState(Type state, Type initialState) + { + if (!state.IsAssignableTo(typeof(StateBase))) + { + throw new InvalidStateException(state); + } + if (!initialState.IsAssignableTo(typeof(IInitialState<>).MakeGenericType(state))) + { + throw new InvalidInitialStateException(state, initialState); + } + _states.Add(state); + _initialStates.Add(initialState); + return this; + } + + public IStatorBuilder AddAction(Type actionType) + { + if (!IsAction(actionType)) + { + throw new InvalidActionException(actionType); + } + _actions.Add(actionType); + return this; + } + + public IStatorBuilder AddUpdaters(Type updaterType) + { + if (!IsUpdater(updaterType)) + { + throw new InvalidUpdaterException(updaterType); + } + _updaters.Add(updaterType); + return this; + } + + public IStatorBuilder AddActionFilter(Type actionFilterType) + { + if (!IsActionFilter(actionFilterType)) + { + throw new InvalidActionFilterException(actionFilterType); + } + _actionFilters.Add(actionFilterType); + return this; + } + + private static bool IsAction(Type actionType) + => HasGenericInterface(actionType, typeof(IAction<>)); + private static bool IsUpdater(Type updaterType) + => HasGenericInterface(updaterType, typeof(IUpdater<,>)); + private static bool IsActionFilter(Type actionFilterType) + => HasGenericInterface(actionFilterType, typeof(IActionFilter<,>)); + + private static bool HasGenericInterface(Type type, Type interfaceType) + { + var interfaces = type.GetInterfaces() + .Count(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + return interfaces > 0; + } } + +public class InvalidStateException : Exception +{ + public InvalidStateException(Type stateType) + { + StateType = stateType; + } + + public Type StateType { get; } +} + +public class InvalidInitialStateException : Exception +{ + public InvalidInitialStateException(Type stateType, Type initialState) + : base($"The type {initialState.Name} is not a valid IInitialState<{stateType.Name}>.") + { + StateType = stateType; + InitialStateType = initialState; + } + + public Type StateType { get; } + public Type InitialStateType { get; } +} + +public class InvalidActionException : Exception +{ + public InvalidActionException(Type actionType) + : base($"The type {actionType.Name} is not a valid IAction.") + { + ActionType = actionType; + } + + public Type ActionType { get; } +} + +public class InvalidUpdaterException : Exception +{ + public InvalidUpdaterException(Type updaterType) + : base($"The type {updaterType.Name} is not a valid IUpdater.") + { + UpdaterType = updaterType; + } + + public Type UpdaterType { get; } +} + +public class InvalidActionFilterException : Exception +{ + public InvalidActionFilterException(Type actionFilterType) + : base($"The type {actionFilterType.Name} is not a valid IActionFilter.") + { + ActionFilterType = actionFilterType; + } + + public Type ActionFilterType { get; } +} \ No newline at end of file diff --git a/src/StateR/Store.cs b/src/StateR/Internal/Store.cs similarity index 78% rename from src/StateR/Store.cs rename to src/StateR/Internal/Store.cs index 8d1e219..2a8d67a 100644 --- a/src/StateR/Store.cs +++ b/src/StateR/Internal/Store.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace StateR; +namespace StateR.Internal; public class Store : IStore { @@ -13,7 +13,14 @@ public Store(IServiceProvider serviceProvider, IDispatcher dispatcher) _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); } - public Task DispatchAsync(TAction action, CancellationToken cancellationToken = default) where TAction : IAction + public Task DispatchAsync(TAction action, CancellationToken cancellationToken = default) + where TAction : IAction + where TState : StateBase + { + return _dispatcher.DispatchAsync(action, cancellationToken); + } + + public Task DispatchAsync(object action, CancellationToken cancellationToken) { return _dispatcher.DispatchAsync(action, cancellationToken); } diff --git a/src/StateR/Internal/TypeScannerBuilderExtensions.cs b/src/StateR/Internal/TypeScannerBuilderExtensions.cs deleted file mode 100644 index aef5c0c..0000000 --- a/src/StateR/Internal/TypeScannerBuilderExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -using StateR.ActionHandlers; -using StateR.Updaters; -using System.Reflection; - -namespace StateR.Internal; - -public static class TypeScannerBuilderExtensions -{ - public static IStatorBuilder ScanTypes(this IStatorBuilder builder) - { - var states = TypeScanner.FindStates(builder.All); - builder.AddStates(states); - - var actions = TypeScanner.FindActions(builder.All); - builder.AddActions(actions); - - var updaters = TypeScanner.FindUpdaters(builder.All); - builder.AddUpdaters(updaters); - - var actionHandlers = TypeScanner.FindActionHandlers(builder.All); - builder.AddUpdaters(actionHandlers); - - return builder; - } -} -public static class TypeScanner -{ - public static IEnumerable FindStates(IEnumerable types) - { - var states = types - .Where(type => !type.IsAbstract && type.IsSubclassOf(typeof(StateBase))); - return states; - } - - public static IEnumerable FindActions(IEnumerable types) - { - var actions = types - .Where(type => !type.IsAbstract && type - .GetTypeInfo() - .GetInterfaces() - .Any(i => i == typeof(IAction)) - ); - return actions; - } - - public static IEnumerable FindUpdaters(IEnumerable types) - { - var updaters = types - .Where(type => !type.IsAbstract && type - .GetTypeInfo() - .GetInterfaces() - .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IUpdater<,>)) - ); - return updaters; - } - public static IEnumerable FindActionHandlers(IEnumerable types) - { - var handlers = types - .Where(type => !type.IsAbstract && type - .GetTypeInfo() - .GetInterfaces() - .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IActionHandler<>)) - ); - return handlers; - } - - //public IStatorBuilder FindInterceptors(this IStatorBuilder builder) - //{ - // var iActionInterceptor = typeof(IInterceptor<>); - // return types.Where(type => !type.IsAbstract && type - // .GetTypeInfo() - // .GetInterfaces() - // .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == iActionInterceptor) - // ); - //} - - //public IStatorBuilder FindAfterEffects(this IStatorBuilder builder) - //{ - // var iAfterEffects = typeof(IAfterEffects<>); - // return types.Where(type => !type.IsAbstract && type - // .GetTypeInfo() - // .GetInterfaces() - // .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == iAfterEffects) - // ); - //} -} \ No newline at end of file diff --git a/src/StateR/Pipeline/IActionFilter.cs b/src/StateR/Pipeline/IActionFilter.cs new file mode 100644 index 0000000..2b97cc9 --- /dev/null +++ b/src/StateR/Pipeline/IActionFilter.cs @@ -0,0 +1,13 @@ + +namespace StateR.Pipeline; + +public interface IActionFilter + where TAction : IAction + where TState : StateBase +{ + Task InvokeAsync(IDispatchContext context, ActionDelegate? next, CancellationToken cancellationToken); +} + +public delegate Task ActionDelegate(IDispatchContext context, CancellationToken cancellationToken) + where TAction : IAction + where TState : StateBase; diff --git a/src/StateR/Pipeline/IPipelineFactory.cs b/src/StateR/Pipeline/IPipelineFactory.cs new file mode 100644 index 0000000..0158880 --- /dev/null +++ b/src/StateR/Pipeline/IPipelineFactory.cs @@ -0,0 +1,38 @@ + +using Microsoft.Extensions.DependencyInjection; +using StateR.Internal; + +namespace StateR.Pipeline; + +public interface IPipelineFactory +{ + ActionDelegate Create(IDispatchContext context) + where TAction : IAction + where TState : StateBase; +} + +public class PipelineFactory : IPipelineFactory +{ + private readonly IServiceProvider _serviceProvider; + public PipelineFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + public ActionDelegate Create(IDispatchContext context) + where TAction : IAction + where TState : StateBase + { + var filters = _serviceProvider.GetServices>(); + var enumerator = filters.GetEnumerator(); + enumerator.MoveNext(); + return MakeDelegate(enumerator.Current); + + ActionDelegate MakeDelegate(IActionFilter filter) + { + var hasNext = enumerator.MoveNext(); + Console.WriteLine($"ActionDelegate: {filter.GetType().GetStatorName()}"); + return new((a, s) => filter.InvokeAsync(a, hasNext ? MakeDelegate(enumerator.Current) : null, s)); + } + } +} \ No newline at end of file diff --git a/src/StateR/StateR.csproj b/src/StateR/StateR.csproj index 37d1c0a..ad8857a 100644 --- a/src/StateR/StateR.csproj +++ b/src/StateR/StateR.csproj @@ -9,7 +9,6 @@ - diff --git a/src/StateR/StatorStartupExtensions.cs b/src/StateR/StatorStartupExtensions.cs deleted file mode 100644 index 093f7bd..0000000 --- a/src/StateR/StatorStartupExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using StateR.ActionHandlers; -using StateR.ActionHandlers.Hooks; -using StateR.AfterEffects; -using StateR.AfterEffects.Hooks; -using StateR.Interceptors; -using StateR.Interceptors.Hooks; -using StateR.Internal; -using StateR.Updaters; -using StateR.Updaters.Hooks; -using System.Reflection; - -namespace StateR; - -public static class StatorStartupExtensions -{ - public static IStatorBuilder AddStateR(this IServiceCollection services) - { - services.TryAddSingleton(); - services.TryAddSingleton(); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); - - return new StatorBuilder(services); - } - - public static IStatorBuilder AddStateR(this IServiceCollection services, params Assembly[] assembliesToScan) - { - var builder = services.AddStateR(); - var allTypes = assembliesToScan - .SelectMany(a => a.GetTypes()); - return builder.AddTypes(allTypes); - } - - public static IServiceCollection Apply(this IStatorBuilder builder, Action? postConfiguration = null) - { - // Extract types - builder.ScanTypes(); - - // Scan - builder.Services.Scan(s => s - .AddTypes(builder.All) - - // Equivalent to: AddSingleton, Implementation>(); - .AddClasses(classes => classes.AssignableTo(typeof(IInitialState<>))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IBeforeInterceptorHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IAfterInterceptorHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IBeforeAfterEffectHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IAfterAfterEffectHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IBeforeActionHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IAfterActionHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IBeforeUpdateHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - // Equivalent to: AddSingleton(); - .AddClasses(classes => classes.AssignableTo(typeof(IAfterUpdateHook))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton, Implementation>(); - .AddClasses(classes => classes.AssignableTo(typeof(IInterceptor<>))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - - // Equivalent to: AddSingleton, Implementation>(); - .AddClasses(classes => classes.AssignableTo(typeof(IAfterEffects<>))) - .AsImplementedInterfaces() - .WithSingletonLifetime() - ); - - // Register States - foreach (var state in builder.States) - { - Console.WriteLine($"state: {state.FullName}"); - - // Equivalent to: AddSingleton, State>(); - var stateServiceType = typeof(IState<>).MakeGenericType(state); - var stateImplementationType = typeof(State<>).MakeGenericType(state); - builder.Services.AddSingleton(stateServiceType, stateImplementationType); - } - - // Register Updaters and their respective IActionHandler - var iUpdaterType = typeof(IUpdater<,>); - var updaterHandler = typeof(UpdaterActionHandler<,>); - var handlerType = typeof(IActionHandler<>); - foreach (var updater in builder.Updaters) - { - Console.WriteLine($"updater: {updater.FullName}"); - var interfaces = updater.GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == iUpdaterType); - foreach (var @interface in interfaces) - { - // Equivalent to: AddSingleton, UpdaterHandler> - var actionType = @interface.GenericTypeArguments[0]; - var stateType = @interface.GenericTypeArguments[1]; - var iActionHandlerServiceType = handlerType.MakeGenericType(actionType); - var updaterHandlerImplementationType = updaterHandler.MakeGenericType(stateType, actionType); - builder.Services.AddSingleton(iActionHandlerServiceType, updaterHandlerImplementationType); - - // Equivalent to: AddSingleton, Updater>(); - builder.Services.AddSingleton(@interface, updater); - - Console.WriteLine($"- AddSingleton<{iActionHandlerServiceType.GetStatorName()}, {updaterHandlerImplementationType.GetStatorName()}>()"); - Console.WriteLine($"- AddSingleton<{@interface.GetStatorName()}, {updater.GetStatorName()}>()"); - } - } - - // Run post-configuration - postConfiguration?.Invoke(builder); - - return builder.Services; - } -} diff --git a/src/StateR/Updaters/Hooks/IAfterUpdateHook.cs b/src/StateR/Updaters/Hooks/IAfterUpdateHook.cs deleted file mode 100644 index bcc066a..0000000 --- a/src/StateR/Updaters/Hooks/IAfterUpdateHook.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StateR.Updaters.Hooks; - -public interface IAfterUpdateHook -{ - Task AfterUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase; -} diff --git a/src/StateR/Updaters/Hooks/IBeforeUpdateHook.cs b/src/StateR/Updaters/Hooks/IBeforeUpdateHook.cs deleted file mode 100644 index 7bd32a7..0000000 --- a/src/StateR/Updaters/Hooks/IBeforeUpdateHook.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StateR.Updaters.Hooks; - -public interface IBeforeUpdateHook -{ - Task BeforeUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase; -} diff --git a/src/StateR/Updaters/Hooks/IUpdateHooksCollection.cs b/src/StateR/Updaters/Hooks/IUpdateHooksCollection.cs deleted file mode 100644 index 1503e95..0000000 --- a/src/StateR/Updaters/Hooks/IUpdateHooksCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace StateR.Updaters.Hooks; - -public interface IUpdateHooksCollection -{ - Task BeforeUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase; - Task AfterUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase; -} diff --git a/src/StateR/Updaters/Hooks/UpdateHooksCollection.cs b/src/StateR/Updaters/Hooks/UpdateHooksCollection.cs deleted file mode 100644 index e8c56d8..0000000 --- a/src/StateR/Updaters/Hooks/UpdateHooksCollection.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace StateR.Updaters.Hooks; - -public class UpdateHooksCollection : IUpdateHooksCollection -{ - private readonly IEnumerable _beforeUpdateHooks; - private readonly IEnumerable _afterUpdateHooks; - public UpdateHooksCollection(IEnumerable beforeUpdateHooks, IEnumerable afterUpdateHooks) - { - _beforeUpdateHooks = beforeUpdateHooks ?? throw new ArgumentNullException(nameof(beforeUpdateHooks)); - _afterUpdateHooks = afterUpdateHooks ?? throw new ArgumentNullException(nameof(afterUpdateHooks)); - } - - public async Task BeforeUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase - { - foreach (var hook in _beforeUpdateHooks) - { - await hook.BeforeUpdateAsync(context, state, updater, cancellationToken); - } - } - - public async Task AfterUpdateAsync(IDispatchContext context, IState state, IUpdater updater, CancellationToken cancellationToken) - where TAction : IAction - where TState : StateBase - { - foreach (var hook in _afterUpdateHooks) - { - await hook.AfterUpdateAsync(context, state, updater, cancellationToken); - } - } -} diff --git a/src/StateR/Updaters/IUpdater.cs b/src/StateR/Updaters/IUpdater.cs index ddeacdc..6882cd0 100644 --- a/src/StateR/Updaters/IUpdater.cs +++ b/src/StateR/Updaters/IUpdater.cs @@ -1,7 +1,7 @@ namespace StateR.Updaters; public interface IUpdater - where TAction : IAction + where TAction : IAction where TState : StateBase { TState Update(TAction action, TState state); diff --git a/src/StateR/Updaters/UpdaterActionHandler.cs b/src/StateR/Updaters/UpdaterActionHandler.cs deleted file mode 100644 index 33efa7b..0000000 --- a/src/StateR/Updaters/UpdaterActionHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using StateR.ActionHandlers; -using StateR.Updaters.Hooks; - -namespace StateR.Updaters; - -public class UpdaterActionHandler : IActionHandler - where TState : StateBase - where TAction : IAction -{ - private readonly IUpdateHooksCollection _hooks; - private readonly IEnumerable> _updaters; - private readonly IState _state; - - public UpdaterActionHandler(IState state, IEnumerable> updaters, IUpdateHooksCollection hooks) - { - _state = state ?? throw new ArgumentNullException(nameof(state)); - _updaters = updaters ?? throw new ArgumentNullException(nameof(updaters)); - _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); - } - - public async Task HandleAsync(IDispatchContext context, CancellationToken cancellationToken) - { - foreach (var updater in _updaters) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } - await _hooks.BeforeUpdateAsync(context, _state, updater, cancellationToken); - if (cancellationToken.IsCancellationRequested) - { - break; - } - _state.Set(updater.Update(context.Action, _state.Current)); - await _hooks.AfterUpdateAsync(context, _state, updater, cancellationToken); - } - _state.Notify(); - cancellationToken.ThrowIfCancellationRequested(); - } -} diff --git a/src/StateR/Updaters/UpdaterMiddleware.cs b/src/StateR/Updaters/UpdaterMiddleware.cs new file mode 100644 index 0000000..60183a3 --- /dev/null +++ b/src/StateR/Updaters/UpdaterMiddleware.cs @@ -0,0 +1,34 @@ +using StateR.Pipeline; + +namespace StateR.Updaters; + +public class UpdaterMiddleware : IActionFilter + where TState : StateBase + where TAction : IAction +{ + private readonly IEnumerable> _updaters; + private readonly IState _state; + + public UpdaterMiddleware(IState state, IEnumerable> updaters) + { + _state = state ?? throw new ArgumentNullException(nameof(state)); + _updaters = updaters ?? throw new ArgumentNullException(nameof(updaters)); + } + + public Task InvokeAsync(IDispatchContext context, ActionDelegate? next, CancellationToken cancellationToken) + { + foreach (var updater in _updaters) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + _state.Set(updater.Update(context.Action, _state.Current)); + } + _state.Notify(); + cancellationToken.ThrowIfCancellationRequested(); + + next?.Invoke(context, cancellationToken); + return Task.CompletedTask; + } +} diff --git a/test/StateR.Tests/ActionHandlers/ActionHandlerManagerTest.cs b/test/StateR.Tests/ActionHandlers/ActionHandlerManagerTest.cs deleted file mode 100644 index ff259c1..0000000 --- a/test/StateR.Tests/ActionHandlers/ActionHandlerManagerTest.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Moq; -using StateR.ActionHandlers.Hooks; -using Xunit; - -namespace StateR.ActionHandlers; - -public class ActionHandlerManagerTest -{ - private readonly Mock _hooksCollectionMock = new(); - - protected ActionHandlersManager CreateUpdatersManager(Action configureServices) - { - var services = new ServiceCollection(); - configureServices?.Invoke(services); - var serviceProvider = services.BuildServiceProvider(); - return new ActionHandlersManager(_hooksCollectionMock.Object, serviceProvider); - } - - public class DispatchAsync : ActionHandlerManagerTest - { - [Fact] - public async Task Should_call_all_action_handlers() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var handler1 = new Mock>(); - var handler2 = new Mock>(); - var sut = CreateUpdatersManager(services => - { - services.AddSingleton(handler1.Object); - services.AddSingleton(handler2.Object); - }); - - // Act - await sut.DispatchAsync(context); - - // Assert - handler1.Verify(x => x.HandleAsync(context, cancellationTokenSource.Token), Times.Once); - handler2.Verify(x => x.HandleAsync(context, cancellationTokenSource.Token), Times.Once); - } - - [Fact] - public async Task Should_break_handlers_when_Cancel() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var afterEffect1 = new Mock>(); - afterEffect1.Setup(x => x.HandleAsync(context, cancellationTokenSource.Token)) - .Callback((IDispatchContext context, CancellationToken cancellationToken) - => context.Cancel()); - var afterEffect2 = new Mock>(); - var sut = CreateUpdatersManager(services => - { - services.AddSingleton(afterEffect1.Object); - services.AddSingleton(afterEffect2.Object); - }); - - // Act - await Assert.ThrowsAsync(() - => sut.DispatchAsync(context)); - - // Assert - afterEffect1.Verify(x => x.HandleAsync(context, cancellationTokenSource.Token), Times.Once); - afterEffect2.Verify(x => x.HandleAsync(context, cancellationTokenSource.Token), Times.Never); - } - - [Fact] - public async Task Should_call_middleware_and_handlers_in_order() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var operationQueue = new Queue(); - var actionHandler1 = new Mock>(); - actionHandler1.Setup(x => x.HandleAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("actionHandler1.HandleAsync")); - var actionHandler2 = new Mock>(); - actionHandler2.Setup(x => x.HandleAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("actionHandler2.HandleAsync")); - _hooksCollectionMock - .Setup(x => x.BeforeHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("BeforeHandlerAsync")); - _hooksCollectionMock - .Setup(x => x.AfterHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("AfterHandlerAsync")); - - var sut = CreateUpdatersManager(services => - { - services.AddSingleton(actionHandler1.Object); - services.AddSingleton(actionHandler2.Object); - }); - - // Act - await sut.DispatchAsync(context); - - // Assert - Assert.Collection(operationQueue, - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("actionHandler1.HandleAsync", op), - op => Assert.Equal("AfterHandlerAsync", op), - - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("actionHandler2.HandleAsync", op), - op => Assert.Equal("AfterHandlerAsync", op) - ); - } - } - - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/ActionHandlers/Hooks/ActionHandlerHooksCollectionTest.cs b/test/StateR.Tests/ActionHandlers/Hooks/ActionHandlerHooksCollectionTest.cs deleted file mode 100644 index 50a60fb..0000000 --- a/test/StateR.Tests/ActionHandlers/Hooks/ActionHandlerHooksCollectionTest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Moq; -using Xunit; - -namespace StateR.ActionHandlers.Hooks; - -public class ActionHandlerHooksCollectionTest -{ - private readonly Mock _before1Mock = new(); - private readonly Mock _before2Mock = new(); - private readonly Mock _after1Mock = new(); - private readonly Mock _after2Mock = new(); - - private readonly Mock> _afterEffectMock = new(); - private readonly IDispatchContext dispatchContext; - private readonly CancellationToken _cancellationToken = CancellationToken.None; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly ActionHandlerHooksCollection sut; - - public ActionHandlerHooksCollectionTest() - { - dispatchContext = new DispatchContext(new TestAction(), new Mock().Object, _cancellationTokenSource); - sut = new( - new[] { _before1Mock.Object, _before2Mock.Object }, - new[] { _after1Mock.Object, _after2Mock.Object } - ); - } - public class BeforeHandlerAsync : ActionHandlerHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.BeforeHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _before2Mock.Verify(x => x.BeforeHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after1Mock.Verify(x => x.AfterHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after2Mock.Verify(x => x.AfterHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - } - } - - public class AfterHandlerAsync : ActionHandlerHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.AfterHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _before2Mock.Verify(x => x.BeforeHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after1Mock.Verify(x => x.AfterHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after2Mock.Verify(x => x.AfterHandlerAsync(dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - } - } - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/AfterEffects/AfterEffectsManagerTest.cs b/test/StateR.Tests/AfterEffects/AfterEffectsManagerTest.cs deleted file mode 100644 index bd9a9b1..0000000 --- a/test/StateR.Tests/AfterEffects/AfterEffectsManagerTest.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Moq; -using StateR.AfterEffects.Hooks; -using Xunit; - -namespace StateR.AfterEffects; - -public class AfterEffectsManagerTest -{ - private readonly Mock _afterEffectHooksCollectionMock = new(); - protected AfterEffectsManager CreateAfterEffectsManager(Action configureServices) - { - var services = new ServiceCollection(); - configureServices?.Invoke(services); - services.TryAddSingleton(_afterEffectHooksCollectionMock.Object); - var serviceProvider = services.BuildServiceProvider(); - var afterEffectHooksCollection = serviceProvider.GetService(); - return new AfterEffectsManager(afterEffectHooksCollection, serviceProvider); - } - - public class DispatchAsync : AfterEffectsManagerTest - { - [Fact] - public async Task Should_handle_all_after_effects() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var afterEffect1 = new Mock>(); - var afterEffect2 = new Mock>(); - var sut = CreateAfterEffectsManager(services => - { - services.AddSingleton(afterEffect1.Object); - services.AddSingleton(afterEffect2.Object); - }); - - // Act - await sut.DispatchAsync(context); - - // Assert - afterEffect1.Verify(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token), Times.Once); - afterEffect2.Verify(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token), Times.Once); - } - - [Fact] - public async Task Should_break_after_effects_when_Cancel() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var afterEffect1 = new Mock>(); - afterEffect1.Setup(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token)) - .Callback((IDispatchContext context, CancellationToken cancellationToken) => context.Cancel()); - var afterEffect2 = new Mock>(); - var sut = CreateAfterEffectsManager(services => - { - services.AddSingleton(afterEffect1.Object); - services.AddSingleton(afterEffect2.Object); - }); - - // Act - await Assert.ThrowsAsync(() - => sut.DispatchAsync(context)); - - // Assert - afterEffect1.Verify(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token), Times.Once); - afterEffect2.Verify(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token), Times.Never); - } - - [Fact] - public async Task Should_call_hooks_and_after_effects_methods_in_order() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var operationQueue = new Queue(); - var afterEffect1 = new Mock>(); - afterEffect1.Setup(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("afterEffect1.HandleAfterEffectAsync")); - var afterEffect2 = new Mock>(); - afterEffect2.Setup(x => x.HandleAfterEffectAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("afterEffect2.HandleAfterEffectAsync")); - _afterEffectHooksCollectionMock - .Setup(x => x.BeforeHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("BeforeHandlerAsync")); - _afterEffectHooksCollectionMock - .Setup(x => x.AfterHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("AfterHandlerAsync")); - var sut = CreateAfterEffectsManager(services => - { - services.AddSingleton(afterEffect1.Object); - services.AddSingleton(afterEffect2.Object); - }); - - // Act - await sut.DispatchAsync(context); - - // Assert - Assert.Collection(operationQueue, - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("afterEffect1.HandleAfterEffectAsync", op), - op => Assert.Equal("AfterHandlerAsync", op), - - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("afterEffect2.HandleAfterEffectAsync", op), - op => Assert.Equal("AfterHandlerAsync", op) - ); - } - } - - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/AfterEffects/Hooks/AfterEffectHooksCollectionTest.cs b/test/StateR.Tests/AfterEffects/Hooks/AfterEffectHooksCollectionTest.cs deleted file mode 100644 index baa3fea..0000000 --- a/test/StateR.Tests/AfterEffects/Hooks/AfterEffectHooksCollectionTest.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Moq; -using Xunit; - -namespace StateR.AfterEffects.Hooks; - -public class AfterEffectHooksCollectionTest -{ - private readonly Mock _before1Mock = new(); - private readonly Mock _before2Mock = new(); - private readonly Mock _after1Mock = new(); - private readonly Mock _after2Mock = new(); - - private readonly Mock> _afterEffectMock = new(); - private readonly IDispatchContext _dispatchContext; - private readonly CancellationToken _cancellationToken = CancellationToken.None; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly AfterEffectHooksCollection sut; - - public AfterEffectHooksCollectionTest() - { - _dispatchContext = new DispatchContext(new TestAction(), new Mock().Object, _cancellationTokenSource); - sut = new( - new[] { _before1Mock.Object, _before2Mock.Object }, - new[] { _after1Mock.Object, _after2Mock.Object } - ); - } - public class BeforeHandlerAsync : AfterEffectHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _before2Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after1Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after2Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - } - - } - - public class AfterHandlerAsync : AfterEffectHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _before2Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after1Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after2Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - } - } - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/DispatcherTest.cs b/test/StateR.Tests/DispatcherTest.cs deleted file mode 100644 index 42c81d3..0000000 --- a/test/StateR.Tests/DispatcherTest.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Microsoft.Extensions.Logging; -using Moq; -using StateR.ActionHandlers; -using StateR.AfterEffects; -using StateR.Interceptors; -using Xunit; - -namespace StateR; - -public class DispatcherTest -{ - private readonly Mock _dispatchContextFactory = new(); - private readonly Mock _interceptorsManager = new(); - private readonly Mock _actionHandlersManager = new(); - private readonly Mock _afterEffectsManager = new(); - private readonly Mock> _loggerMock = new(); - private readonly Dispatcher sut; - - public DispatcherTest() - { - sut = new(_dispatchContextFactory.Object, _interceptorsManager.Object, _actionHandlersManager.Object, _afterEffectsManager.Object, _loggerMock.Object); - } - - public class DispatchAsync : DispatcherTest - { - [Fact] - public async Task Should_create_DispatchContext_using_dispatchContextFactory() - { - var action = new TestAction(); - await sut.DispatchAsync(action, CancellationToken.None); - _dispatchContextFactory - .Verify(x => x.Create(action, sut, It.IsAny()), Times.Once); - } - - [Fact] - public async Task Should_send_the_same_DispatchContext_to_all_managers() - { - // Arrange - var action = new TestAction(); - var context = new DispatchContext(action, new Mock().Object, new CancellationTokenSource()); - _dispatchContextFactory - .Setup(x => x.Create(action, sut, It.IsAny())) - .Returns(context); - - // Act - await sut.DispatchAsync(action, CancellationToken.None); - - // Assert - _interceptorsManager.Verify(x => x.DispatchAsync(context), Times.Once); - _actionHandlersManager.Verify(x => x.DispatchAsync(context), Times.Once); - _afterEffectsManager.Verify(x => x.DispatchAsync(context), Times.Once); - } - [Fact] - public async Task Should_call_managers_in_the_expected_order() - { - // Arrange - var action = new TestAction(); - var operationQueue = new Queue(); - _interceptorsManager - .Setup(x => x.DispatchAsync(It.IsAny>())) - .Callback(() => operationQueue.Enqueue("Interceptors")); - _actionHandlersManager - .Setup(x => x.DispatchAsync(It.IsAny>())) - .Callback(() => operationQueue.Enqueue("Updaters")); - _afterEffectsManager - .Setup(x => x.DispatchAsync(It.IsAny>())) - .Callback(() => operationQueue.Enqueue("AfterEffects")); - - // Act - await sut.DispatchAsync(action, CancellationToken.None); - - // Assert - Assert.Collection(operationQueue, - operation => Assert.Equal("Interceptors", operation), - operation => Assert.Equal("Updaters", operation), - operation => Assert.Equal("AfterEffects", operation) - ); - } - } - - private record TestAction : IAction; -} diff --git a/test/StateR.Tests/IntegrationTest.cs b/test/StateR.Tests/IntegrationTest.cs new file mode 100644 index 0000000..4bc6853 --- /dev/null +++ b/test/StateR.Tests/IntegrationTest.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.DependencyInjection; +using StateR.Internal; +using StateR.Pipeline; +using StateR.Updaters; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StateR; +public class IntegrationTest +{ + public record class CounterState(int Count) : StateBase; + public record class InitialCounterState : IInitialState + { + public CounterState Value => new(0); + } + public record class Increment(int Amount) : IAction; + public record class Decrement(int Amount) : IAction; + public class CounterUpdaters : + IUpdater, + IUpdater + { + public CounterState Update(Increment action, CounterState state) + => state with { Count = state.Count + action.Amount }; + public CounterState Update(Decrement action, CounterState state) + => state with { Count = state.Count - action.Amount }; + } + public class ValidateIncrementFilter : IActionFilter + { + public Task InvokeAsync( + IDispatchContext context, + ActionDelegate next, + CancellationToken cancellationToken) + { + if (context.Action.Amount <= 0) + { + throw new ValidationException(); + } + return next?.Invoke(context, cancellationToken); + } + } + public class ValidationException : Exception { } + + public class IncrementTest : IntegrationTest + { + [Fact] + public async Task Should_increment_the_CounterState_by_the_Increment_action_Amount() + { + // Arrange + var services = Initialize(); + var state = services.GetRequiredService>(); + var initialCount = state.Current.Count; + Assert.Equal(0, initialCount); + + var cancellationToken = CancellationToken.None; + var dispatcher = services.GetRequiredService(); + + // Act + await dispatcher.DispatchAsync(new Increment(2), cancellationToken); + + // Assert + Assert.Equal(2, state.Current.Count); + } + } + + public class DecrementTest : IntegrationTest + { + [Fact] + public async Task Should_decrement_the_CounterState_by_the_Increment_action_Amount() + { + // Arrange + var services = Initialize(); + var state = services.GetRequiredService>(); + var initialCount = state.Current.Count; + Assert.Equal(0, initialCount); + + var cancellationToken = CancellationToken.None; + var dispatcher = services.GetRequiredService(); + + // Act + await dispatcher.DispatchAsync(new Decrement(5), cancellationToken); + + // Assert + Assert.Equal(-5, state.Current.Count); + } + } + + public class ValidateIncrementFilterTest : IntegrationTest + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task Should_throw_a_ValidationException_when_Increment_is_smaller_or_equal_to_zero(int amount) + { + // Arrange + var services = Initialize(); + var cancellationToken = CancellationToken.None; + var dispatcher = services.GetRequiredService(); + + // Act & Assert + await Assert.ThrowsAsync(() => dispatcher + .DispatchAsync(new Increment(amount), cancellationToken)); + } + } + + private IServiceProvider Initialize() + { + var services = new ServiceCollection(); + services.AddLogging(); + return services.AddStateR() + .AddState() + .AddAction(typeof(Increment)) + .AddAction(typeof(Decrement)) + .AddUpdaters(typeof(CounterUpdaters)) + .AddActionFilter(typeof(ValidateIncrementFilter)) + .Apply() + .BuildServiceProvider() + ; + } +} diff --git a/test/StateR.Tests/Interceptors/Hooks/InterceptorsHooksCollectionTest.cs b/test/StateR.Tests/Interceptors/Hooks/InterceptorsHooksCollectionTest.cs deleted file mode 100644 index b9cb112..0000000 --- a/test/StateR.Tests/Interceptors/Hooks/InterceptorsHooksCollectionTest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Moq; -using Xunit; - -namespace StateR.Interceptors.Hooks; - -public class InterceptorsHooksCollectionTest -{ - private readonly Mock _before1Mock = new(); - private readonly Mock _before2Mock = new(); - private readonly Mock _after1Mock = new(); - private readonly Mock _after2Mock = new(); - - private readonly Mock> _afterEffectMock = new(); - private readonly IDispatchContext _dispatchContext; - private readonly CancellationToken _cancellationToken = CancellationToken.None; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly InterceptorsHooksCollection sut; - - public InterceptorsHooksCollectionTest() - { - _dispatchContext = new DispatchContext(new TestAction(), new Mock().Object, _cancellationTokenSource); - sut = new( - new[] { _before1Mock.Object, _before2Mock.Object }, - new[] { _after1Mock.Object, _after2Mock.Object } - ); - } - - public class BeforeHandlerAsync : InterceptorsHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _before2Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after1Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after2Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - } - } - public class AfterHandlerAsync : InterceptorsHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _before2Mock.Verify(x => x.BeforeHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Never); - _after1Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - _after2Mock.Verify(x => x.AfterHandlerAsync(_dispatchContext, _afterEffectMock.Object, _cancellationToken), Times.Once); - } - } - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/Interceptors/InterceptorsManagerTest.cs b/test/StateR.Tests/Interceptors/InterceptorsManagerTest.cs deleted file mode 100644 index 09e4521..0000000 --- a/test/StateR.Tests/Interceptors/InterceptorsManagerTest.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Moq; -using StateR.Interceptors.Hooks; -using Xunit; - -namespace StateR.Interceptors; - -public class InterceptorsManagerTest -{ - private readonly Mock _hooksCollectionMock = new(); - protected InterceptorsManager CreateInterceptorsManager(Action configureServices) - { - var services = new ServiceCollection(); - configureServices?.Invoke(services); - services.TryAddSingleton(_hooksCollectionMock.Object); - var serviceProvider = services.BuildServiceProvider(); - return new InterceptorsManager(_hooksCollectionMock.Object, serviceProvider); - } - - public class DispatchAsync : InterceptorsManagerTest - { - [Fact] - public async Task Should_dispatch_to_all_interceptors() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var interceptor1 = new Mock>(); - var interceptor2 = new Mock>(); - var sut = CreateInterceptorsManager(services => - { - services.AddSingleton(interceptor1.Object); - services.AddSingleton(interceptor2.Object); - }); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - // Act - await sut.DispatchAsync(context); - - // Assert - interceptor1.Verify(x => x.InterceptAsync(context, cancellationTokenSource.Token), Times.Once); - interceptor2.Verify(x => x.InterceptAsync(context, cancellationTokenSource.Token), Times.Once); - } - - [Fact] - public async Task Should_call_middleware_and_interceptors_methods_in_order() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var operationQueue = new Queue(); - var interceptor1 = new Mock>(); - interceptor1.Setup(x => x.InterceptAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("interceptor1.InterceptAsync")); - var interceptor2 = new Mock>(); - interceptor2.Setup(x => x.InterceptAsync(context, cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("interceptor2.InterceptAsync")); - _hooksCollectionMock - .Setup(x => x.BeforeHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("BeforeHandlerAsync")); - _hooksCollectionMock - .Setup(x => x.AfterHandlerAsync(context, It.IsAny>(), cancellationTokenSource.Token)) - .Callback(() => operationQueue.Enqueue("AfterHandlerAsync")); - var sut = CreateInterceptorsManager(services => - { - services.AddSingleton(interceptor1.Object); - services.AddSingleton(interceptor2.Object); - }); - - // Act - await sut.DispatchAsync(context); - - // Assert - Assert.Collection(operationQueue, - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("interceptor1.InterceptAsync", op), - op => Assert.Equal("AfterHandlerAsync", op), - - op => Assert.Equal("BeforeHandlerAsync", op), - op => Assert.Equal("interceptor2.InterceptAsync", op), - op => Assert.Equal("AfterHandlerAsync", op) - ); - } - - [Fact] - public async Task Should_break_interception_when_Cancel() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - - var interceptor1 = new Mock>(); - interceptor1.Setup(x => x.InterceptAsync(context, cancellationTokenSource.Token)) - .Callback((IDispatchContext context, CancellationToken cancellationToken) => context.Cancel()); - var interceptor2 = new Mock>(); - var sut = CreateInterceptorsManager(services => - { - services.AddSingleton(interceptor1.Object); - services.AddSingleton(interceptor2.Object); - }); - - // Act - await Assert.ThrowsAsync(() - => sut.DispatchAsync(context)); - - // Assert - interceptor1.Verify(x => x.InterceptAsync(context, cancellationTokenSource.Token), Times.Once); - interceptor2.Verify(x => x.InterceptAsync(context, cancellationTokenSource.Token), Times.Never); - } - } - - public record TestAction : IAction; -} diff --git a/test/StateR.Tests/Internal/StatorBuilderTest.cs b/test/StateR.Tests/Internal/StatorBuilderTest.cs index 208f67b..91d1a18 100644 --- a/test/StateR.Tests/Internal/StatorBuilderTest.cs +++ b/test/StateR.Tests/Internal/StatorBuilderTest.cs @@ -1,10 +1,181 @@ using Microsoft.Extensions.DependencyInjection; +using System; using Xunit; namespace StateR.Internal; public class StatorBuilderTest { + public class AddState_TState : StatorBuilderTest + { + [Fact] + public void Should_add_TState_to_States_and_TInitialState_to_InitialStates() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act + sut.AddState(); + + // Assert + Assert.Collection(sut.States, + type => Assert.Equal(typeof(TestState1), type) + ); + Assert.Collection(sut.InitialStates, + type => Assert.Equal(typeof(InitialTestState1), type) + ); + } + } + public class AddState_Type : StatorBuilderTest + { + [Fact] + public void Should_add_a_valid_state_type_to_States_and_valid_initialState_to_InitialStates() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act + sut.AddState(typeof(TestState1), typeof(InitialTestState1)); + + // Assert + Assert.Collection(sut.States, + type => Assert.Equal(typeof(TestState1), type) + ); + Assert.Collection(sut.InitialStates, + type => Assert.Equal(typeof(InitialTestState1), type) + ); + } + + [Fact] + public void Should_throw_an_InvalidStateException_when_the_state_type_does_not_inherit_StateBase() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + var stateType = typeof(NotAState); + var initialStateType = typeof(InitialTestState1); + + // Act & Assert + var ex = Assert.Throws(() => sut.AddState(stateType, initialStateType)); + Assert.Same(stateType, ex.StateType); + } + + [Theory] + [InlineData(typeof(TestState1), typeof(InitialTestState2))] + [InlineData(typeof(TestState1), typeof(NotAState))] + public void Should_throw_an_InvalidInitialStateException_when_the_initialState_is_invalid(Type stateType, Type initialStateType) + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act & Assert + var ex = Assert.Throws(() => sut.AddState(stateType, initialStateType)); + Assert.Same(initialStateType, ex.InitialStateType); + } + } + + public class AddAction : StatorBuilderTest + { + [Fact] + public void Should_add_TAction_to_Actions() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + var actionType = typeof(TestAction1); + + // Act + sut.AddAction(actionType); + + // Assert + Assert.Collection(sut.Actions, + type => Assert.Same(actionType, type) + ); + } + + [Theory] + [InlineData(typeof(NotAnAction))] + public void Should_throw_an_InvalidActionException_when_actionType_is_invalid(Type actionType) + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act & Assert + var ex = Assert.Throws(() => sut.AddAction(actionType)); + Assert.Same(actionType, ex.ActionType); + } + } + + public class AddUpdaters : StatorBuilderTest + { + [Fact] + public void Should_add_updaterType_to_Updaters() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + var updaterType = typeof(TestUpdater1); + + // Act + sut.AddUpdaters(updaterType); + + // Assert + Assert.Collection(sut.Updaters, + type => Assert.Same(updaterType, type) + ); + } + + [Theory] + [InlineData(typeof(NotAnUpdater))] + public void Should_throw_an_InvalidUpdaterException_when_updaterType_is_invalid(Type updaterType) + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act & Assert + var ex = Assert.Throws(() => sut.AddUpdaters(updaterType)); + Assert.Same(updaterType, ex.UpdaterType); + } + } + + public class AddActionFilter : StatorBuilderTest + { + [Fact] + public void Should_add_actionFilterType_to_ActionFilters() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + var actionFilterType = typeof(TestActionFilter1); + + // Act + sut.AddActionFilter(actionFilterType); + + // Assert + Assert.Collection(sut.ActionFilters, + type => Assert.Same(actionFilterType, type) + ); + } + + [Theory] + [InlineData(typeof(NotAnActionFilter))] + public void Should_throw_an_InvalidActionFilterException_when_actionFilterType_is_invalid(Type actionFilterType) + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services); + + // Act & Assert + var ex = Assert.Throws(() => sut.AddActionFilter(actionFilterType)); + Assert.Same(actionFilterType, ex.ActionFilterType); + } + } + public class AddTypes : StatorBuilderTest { [Fact] diff --git a/test/StateR.Tests/Reducers/Hooks/UpdateHooksCollectionTest.cs b/test/StateR.Tests/Reducers/Hooks/UpdateHooksCollectionTest.cs deleted file mode 100644 index fd771ad..0000000 --- a/test/StateR.Tests/Reducers/Hooks/UpdateHooksCollectionTest.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Moq; -using Xunit; - -namespace StateR.Updaters.Hooks; - -public class UpdateHooksCollectionTest -{ - private readonly Mock _before1Mock = new(); - private readonly Mock _before2Mock = new(); - private readonly Mock _after1Mock = new(); - private readonly Mock _after2Mock = new(); - - private readonly Mock> _stateMock = new(); - private readonly Mock> _updater = new(); - - private readonly IDispatchContext _dispatchContext; - private readonly CancellationToken _cancellationToken = CancellationToken.None; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly UpdateHooksCollection sut; - - public UpdateHooksCollectionTest() - { - _dispatchContext = new DispatchContext(new TestAction(), new Mock().Object, _cancellationTokenSource); - sut = new UpdateHooksCollection( - new[] { _before1Mock.Object, _before2Mock.Object }, - new[] { _after1Mock.Object, _after2Mock.Object } - ); - } - - public class BeforeUpdateAsync : UpdateHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.BeforeUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Once); - _before2Mock.Verify(x => x.BeforeUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Once); - _after1Mock.Verify(x => x.AfterUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Never); - _after2Mock.Verify(x => x.AfterUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Never); - } - } - - public class AfterUpdateAsync : UpdateHooksCollectionTest - { - [Fact] - public async Task Should_call_all_hooks() - { - // Act - await sut.AfterUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken); - - // Assert - _before1Mock.Verify(x => x.BeforeUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Never); - _before2Mock.Verify(x => x.BeforeUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Never); - _after1Mock.Verify(x => x.AfterUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Once); - _after2Mock.Verify(x => x.AfterUpdateAsync(_dispatchContext, _stateMock.Object, _updater.Object, _cancellationToken), Times.Once); - } - } - - public record TestAction : IAction; - public record TestState : StateBase; -} diff --git a/test/StateR.Tests/Reducers/UpdaterActionHandlerTest.cs b/test/StateR.Tests/Reducers/UpdaterActionHandlerTest.cs deleted file mode 100644 index cd65dba..0000000 --- a/test/StateR.Tests/Reducers/UpdaterActionHandlerTest.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Moq; -using StateR.Updaters.Hooks; -using Xunit; - -namespace StateR.Updaters; - -public class UpdaterActionHandlerTest -{ - private readonly TestState _state = new(); - private readonly Mock> _stateMock = new(); - private readonly List> _updaters = new(); - private readonly Mock _hooksMock = new(); - private readonly UpdaterActionHandler sut; - private readonly Queue _operationQueue = new(); - private readonly TestAction _action = new(); - private readonly DispatchContext _context; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly Mock> _updater1Mock = new(); - private readonly Mock> _updater2Mock = new(); - - public UpdaterActionHandlerTest() - { - _context = new(_action, new Mock().Object, _cancellationTokenSource); - - _stateMock.Setup(x => x.Current).Returns(_state); - _stateMock.Setup(x => x.Notify()) - .Callback(() => _operationQueue.Enqueue("state.Notify")); - - _updater1Mock - .Setup(x => x.Update(_action, _state)) - .Returns(_state) - .Callback(() => _operationQueue.Enqueue("updater1.Update")); - _updaters.Add(_updater1Mock.Object); - _updater2Mock - .Setup(x => x.Update(_action, _state)) - .Returns(_state) - .Callback(() => _operationQueue.Enqueue("updater2.Update")); - _updaters.Add(_updater2Mock.Object); - - sut = new UpdaterActionHandler( - _stateMock.Object, - _updaters, - _hooksMock.Object - ); - } - - public class HandleAsync : UpdaterActionHandlerTest - { - [Fact] - public async Task Should_call_updaters_then_notify() - { - // Act - await sut.HandleAsync(_context, CancellationToken.None); - - // Assert - Assert.Collection(_operationQueue, - op => Assert.Equal("updater1.Update", op), - op => Assert.Equal("updater2.Update", op), - op => Assert.Equal("state.Notify", op) - ); - } - - [Fact] - public async Task Should_break_updates_when_Cancel_is_called_in_a_BeforeUpdateAsync_hook() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("BeforeUpdaterAsync:Updater1")); - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => - { - _operationQueue.Enqueue("BeforeUpdaterAsync:Updater2"); - _cancellationTokenSource.Cancel(); - }); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("AfterUpdaterAsync:Updater1")); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("AfterUpdaterAsync:Updater2")); - - // Act - await Assert.ThrowsAsync(() - => sut.HandleAsync(_context, _cancellationTokenSource.Token)); - - // Assert - Assert.Collection(_operationQueue, - op => Assert.Equal("BeforeUpdaterAsync:Updater1", op), - op => Assert.Equal("updater1.Update", op), - op => Assert.Equal("AfterUpdaterAsync:Updater1", op), - - op => Assert.Equal("BeforeUpdaterAsync:Updater2", op), - - op => Assert.Equal("state.Notify", op) - ); - } - - [Fact] - public async Task Should_break_updates_when_Cancel_is_called_in_an_AfterUpdateAsync_hook() - { - // Arrange - var cancellationTokenSource = new CancellationTokenSource(); - var context = new DispatchContext(new TestAction(), new Mock().Object, cancellationTokenSource); - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("BeforeUpdaterAsync:Updater1")); - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("BeforeUpdaterAsync:Updater2")); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => - { - _operationQueue.Enqueue("AfterUpdaterAsync:Updater1"); - _cancellationTokenSource.Cancel(); - }); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, _cancellationTokenSource.Token)) - .Callback(() => _operationQueue.Enqueue("AfterUpdaterAsync:Updater2")); - - // Act - await Assert.ThrowsAsync(() - => sut.HandleAsync(_context, _cancellationTokenSource.Token)); - - // Assert - Assert.Collection(_operationQueue, - op => Assert.Equal("BeforeUpdaterAsync:Updater1", op), - op => Assert.Equal("updater1.Update", op), - op => Assert.Equal("AfterUpdaterAsync:Updater1", op), - - op => Assert.Equal("state.Notify", op) - ); - } - - [Fact] - public async Task Should_call_hooks_methods_in_order() - { - // Arrange - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, CancellationToken.None)) - .Callback(() => _operationQueue.Enqueue("BeforeUpdaterAsync:Updater1")); - _hooksMock - .Setup(x => x.BeforeUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, CancellationToken.None)) - .Callback(() => _operationQueue.Enqueue("BeforeUpdaterAsync:Updater2")); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater1Mock.Object, CancellationToken.None)) - .Callback(() => _operationQueue.Enqueue("AfterUpdaterAsync:Updater1")); - _hooksMock - .Setup(x => x.AfterUpdateAsync(_context, _stateMock.Object, _updater2Mock.Object, CancellationToken.None)) - .Callback(() => _operationQueue.Enqueue("AfterUpdaterAsync:Updater2")); - - // Act - await sut.HandleAsync(_context, CancellationToken.None); - - // Assert - Assert.Collection(_operationQueue, - op => Assert.Equal("BeforeUpdaterAsync:Updater1", op), - op => Assert.Equal("updater1.Update", op), - op => Assert.Equal("AfterUpdaterAsync:Updater1", op), - - op => Assert.Equal("BeforeUpdaterAsync:Updater2", op), - op => Assert.Equal("updater2.Update", op), - op => Assert.Equal("AfterUpdaterAsync:Updater2", op), - - op => Assert.Equal("state.Notify", op) - ); - } - } - - - public record TestAction : IAction; - public record TestState : StateBase; -} diff --git a/test/StateR.Tests/StateR.Tests.csproj b/test/StateR.Tests/StateR.Tests.csproj index 7709cc4..06ff3ba 100644 --- a/test/StateR.Tests/StateR.Tests.csproj +++ b/test/StateR.Tests/StateR.Tests.csproj @@ -12,6 +12,7 @@ + @@ -26,7 +27,7 @@ - + diff --git a/test/StateR.Tests/StatorStartupExtensionsTest.cs b/test/StateR.Tests/StatorStartupExtensionsTest.cs index 6e042f6..f733f1a 100644 --- a/test/StateR.Tests/StatorStartupExtensionsTest.cs +++ b/test/StateR.Tests/StatorStartupExtensionsTest.cs @@ -1,9 +1,97 @@ -namespace StateR; +using Microsoft.Extensions.DependencyInjection; +using StateR.Internal; +using StateR.Pipeline; +using StateR.Updaters; +using System; +using Xunit; +namespace StateR; public class StatorStartupExtensionsTest { public class AddStateR : StatorStartupExtensionsTest { + [Fact(Skip = "TODO: implement tests")] + public void Should_be_tested() + { + // Arrange + + + // Act + + + // Assert + throw new NotImplementedException(); + } + } + + public class Apply : StatorStartupExtensionsTest + { + [Fact] + public void Should_add_IState_to_the_ServiceCollection() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services) + .AddState() + .AddState() + .AddState() + ; + + // Act + sut.Apply(); + + // Assert + var sp = services.BuildServiceProvider(); + sp.GetRequiredService>(); + sp.GetRequiredService>(); + sp.GetRequiredService>(); + } + + [Fact] + public void Should_add_IUpdater_and_IActionFilter_to_the_ServiceCollection() + { + // Arrange + var services = new ServiceCollection(); + var sut = new StatorBuilder(services) + .AddState() + .AddAction(typeof(TestAction1)) + .AddUpdaters(typeof(TestUpdater1)) + ; + + // Act + sut.Apply(); + + // Assert + var sp = services.BuildServiceProvider(); + sp.GetRequiredService>(); + sp.GetRequiredService>(); + } + + [Fact] + public void Should_add_IActionFilter_to_the_ServiceCollection() + { + // Arrange + var services = new ServiceCollection(); + var sp = new StatorBuilder(services) + .AddState() + .AddAction(typeof(TestAction2)) + .AddUpdaters(typeof(TestUpdater2)) + .AddActionFilter(typeof(TestActionFilter1)) + .AddActionFilter(typeof(TestActionFilter2)) + .Apply() + .BuildServiceProvider(); + ; + + // Act + var actionFilters = sp.GetServices>(); + + // Assert + Assert.Collection(actionFilters, + filter => Assert.IsType>(filter), + filter => Assert.IsType(filter), + filter => Assert.IsType(filter) + ); + } } } diff --git a/test/StateR.Tests/TestTypes.cs b/test/StateR.Tests/TestTypes.cs new file mode 100644 index 0000000..a49465b --- /dev/null +++ b/test/StateR.Tests/TestTypes.cs @@ -0,0 +1,62 @@ +using StateR.Pipeline; +using StateR.Updaters; + +namespace StateR; + +public record class TestState1 : StateBase; +public record class TestState2 : StateBase; +public record class TestState3 : StateBase; + +public record class InitialTestState1 : IInitialState +{ + public TestState1 Value => new(); +} +public record class InitialTestState2 : IInitialState +{ + public TestState2 Value => new(); +} +public record class InitialTestState3 : IInitialState +{ + public TestState3 Value => new(); +} + +public class NotAState { } +public class NotAnAction { } +public class NotAnUpdater { } +public class NotAnActionFilter { } + +public record TestAction1 : IAction; +public record TestAction2 : IAction; + +public class TestUpdater1 : IUpdater +{ + public TestState1 Update(TestAction1 action, TestState1 state) + => new(); +} + +public class TestUpdater2 : IUpdater +{ + public TestState2 Update(TestAction2 action, TestState2 state) + => new(); +} + +public class TestActionFilter1 : IActionFilter +{ + public Task InvokeAsync( + IDispatchContext context, + ActionDelegate next, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} +public class TestActionFilter2 : IActionFilter +{ + public Task InvokeAsync( + IDispatchContext context, + ActionDelegate next, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file