├── docs ├── _config.yml └── img │ └── logging.png ├── publish.cmd ├── src ├── StateInitialized.cs ├── AsyncAction.cs ├── IStateInitializer.cs ├── ILogicFlow.cs ├── Middleware │ ├── IStoreMiddleware.cs │ ├── LogicFlowsStoreMiddleware.cs │ └── JsLoggingStoreMiddleware.cs ├── IStateFlow.cs ├── Rudder.sln ├── RudderExtensions.cs ├── StoreContainer.cs ├── Rudder.csproj ├── StateComponent.cs ├── Store.cs ├── RudderOptions.cs └── Rudder.xml ├── LICENSE.txt ├── .gitignore └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/img/logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kjeske/rudder/HEAD/docs/img/logging.png -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | del src\bin\Release\*.nupkg 3 | dotnet pack src -c Release 4 | nuget push src\bin\Release\*.nupkg -Source https://api.nuget.org/v3/index.json -------------------------------------------------------------------------------- /src/StateInitialized.cs: -------------------------------------------------------------------------------- 1 | namespace Rudder 2 | { 3 | /// 4 | /// Action dispatched after first render of the application 5 | /// 6 | public record StateInitialized; 7 | } 8 | -------------------------------------------------------------------------------- /src/AsyncAction.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Rudder 4 | { 5 | /// 6 | /// Encapsulates a method that has a single parameter and returns a Task. 7 | /// 8 | /// Parameter type 9 | /// Parameter value 10 | public delegate Task AsyncAction(T1 arg1); 11 | } 12 | -------------------------------------------------------------------------------- /src/IStateInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Rudder 4 | { 5 | /// 6 | /// Provides initial state for the application 7 | /// 8 | /// 9 | public interface IStateInitializer 10 | { 11 | /// 12 | /// Provides initial state for the application 13 | /// 14 | /// TState instance 15 | Task GetInitialStateAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ILogicFlow.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Rudder 4 | { 5 | /// 6 | /// Provides handler for an action that will execute the business logic and dispatch further action if needed 7 | /// 8 | public interface ILogicFlow 9 | { 10 | /// 11 | /// Handler for in incoming action for executing the business logic 12 | /// 13 | /// Action that is being currently processed 14 | Task OnNext(object action); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Middleware/IStoreMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Rudder.Middleware 4 | { 5 | /// 6 | /// Provides a custom logic to execute when an action is being processed 7 | /// 8 | public interface IStoreMiddleware 9 | { 10 | /// 11 | /// Logic to execute when an action is being processed 12 | /// 13 | /// Action type 14 | /// Action instance 15 | Task Run(T action); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/IStateFlow.cs: -------------------------------------------------------------------------------- 1 | namespace Rudder 2 | { 3 | /// 4 | /// Provides a handler for an action that will change the state accordingly to the processing action 5 | /// 6 | /// 7 | public interface IStateFlow where TState : class 8 | { 9 | /// 10 | /// Handler for an action that will change the state accordingly to the processing action 11 | /// 12 | /// Current application state 13 | /// Processing action 14 | /// 15 | TState Handle(TState state, object actionValue); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Middleware/LogicFlowsStoreMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Rudder.Middleware 7 | { 8 | /// 9 | /// Provides logic flows execution 10 | /// 11 | public class LogicFlowsStoreMiddleware : IStoreMiddleware 12 | { 13 | private readonly Func> _flows; 14 | 15 | public LogicFlowsStoreMiddleware(Func> flows) 16 | { 17 | _flows = flows; 18 | } 19 | 20 | public async Task Run(T action) => 21 | await Task.WhenAll(_flows().Select(flow => flow.OnNext(action))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Middleware/JsLoggingStoreMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Rudder.Middleware 4 | { 5 | /// 6 | /// Provides JavaScript logging for dispatched actions in a console 7 | /// 8 | public class JsLoggingStoreMiddleware : IStoreMiddleware 9 | { 10 | private readonly IJSRuntime _jsRuntime; 11 | 12 | public JsLoggingStoreMiddleware(IJSRuntime jsRuntime) 13 | { 14 | _jsRuntime = jsRuntime; 15 | } 16 | 17 | public async Task Run(TAction action) 18 | { 19 | try 20 | { 21 | await _jsRuntime.InvokeAsync("console.log", $"%c{GetTypeName(typeof(TAction))}", "font-weight: bold;", action); 22 | } 23 | catch 24 | { 25 | // ignored 26 | } 27 | } 28 | 29 | private static string GetTypeName(Type type) => 30 | type.FullName.Substring(type.Namespace.Length + 1).Replace("+", "."); 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Krzysztof Jeske 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Rudder.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rudder", "Rudder.csproj", "{C5C713BF-3F53-44C0-9BC2-42D106180FFE}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C5C713BF-3F53-44C0-9BC2-42D106180FFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C5C713BF-3F53-44C0-9BC2-42D106180FFE}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C5C713BF-3F53-44C0-9BC2-42D106180FFE}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C5C713BF-3F53-44C0-9BC2-42D106180FFE}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {8C47719A-0A7E-4B22-B385-57EBF406D882} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/RudderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Rudder.Middleware; 4 | 5 | namespace Rudder 6 | { 7 | public static class RudderExtensions 8 | { 9 | /// 10 | /// Configures Rudder library 11 | /// 12 | /// Application state type 13 | /// Services collection 14 | /// Configuration options 15 | /// 16 | public static IServiceCollection AddRudder(this IServiceCollection services, Action> options) 17 | where TState : class 18 | { 19 | if (options == null) 20 | { 21 | throw new ArgumentNullException(nameof(options)); 22 | } 23 | 24 | options(new RudderOptions(services, Assembly.GetCallingAssembly())); 25 | 26 | return services 27 | .AddScoped>>(provider => provider.GetServices) 28 | .AddScoped() 29 | .AddScoped>(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/StoreContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Rudder 5 | { 6 | /// 7 | /// Component used for store initialization 8 | /// 9 | /// State type 10 | public class StoreContainer : IComponent, IHandleAfterRender where TState : class 11 | { 12 | private RenderHandle _renderHandle; 13 | 14 | [Inject] 15 | private Store Store { get; set; } 16 | 17 | [Inject] 18 | private IStateInitializer StateInitializer { get; set; } 19 | 20 | [Parameter] 21 | public RenderFragment ChildContent { get; set; } 22 | 23 | void IComponent.Attach(RenderHandle renderHandle) 24 | { 25 | if (!_renderHandle.IsInitialized) 26 | { 27 | _renderHandle = renderHandle; 28 | } 29 | } 30 | 31 | async Task IComponent.SetParametersAsync(ParameterView parameters) 32 | { 33 | parameters.SetParameterProperties(this); 34 | await Store.Initialize(StateInitializer.GetInitialStateAsync); 35 | _renderHandle.Render(builder => builder.AddContent(0, ChildContent)); 36 | } 37 | 38 | Task IHandleAfterRender.OnAfterRenderAsync() => 39 | Store.PutAsync(new StateInitialized()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Rudder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | enable 5 | enable 6 | Krzysztof Jeske 7 | $([System.DateTime]::op_Subtraction($([System.DateTime]::get_Now().get_Date()),$([System.DateTime]::new(2000,1,1))).get_TotalDays()) 8 | 2.0.5 9 | A state container for server-side Blazor. 10 | Copyright (c) 2022 Krzysztof Jeske 11 | https://github.com/kjeske/rudder 12 | https://github.com/kjeske/rudder.git 13 | GIT 14 | Blazor, State 15 | false 16 | MIT 17 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/StateComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Rudder 4 | { 5 | /// 6 | /// Component base class with access to the state and actions' dispatcher 7 | /// 8 | /// Application state type 9 | /// Component state type 10 | public abstract class StateComponent : ComponentBase, IDisposable 11 | where TState : class 12 | { 13 | private readonly List _unsubscribeActions = new(); 14 | 15 | [Inject] 16 | private Store Store { get; set; } 17 | 18 | /// 19 | /// Dispatches an action 20 | /// 21 | /// Action type 22 | /// Action instance 23 | protected Task PutAsync(T action) => Store.PutAsync(action); 24 | 25 | /// 26 | /// Dispatches an action 27 | /// 28 | /// Action type 29 | /// Action instance 30 | protected void Put(T action) => Store.Put(action); 31 | 32 | protected void UseState(Func mapper) => 33 | _unsubscribeActions.Add(Store.Subscribe(mapper, UpdateState)); 34 | 35 | private void UpdateState() => 36 | Task.Run(() => InvokeAsync(StateHasChanged)); 37 | 38 | public void Dispose() => 39 | _unsubscribeActions.ForEach(unsubscribe => unsubscribe()); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Store.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Rudder.Middleware; 3 | 4 | namespace Rudder 5 | { 6 | /// 7 | /// Provides the application state and dispatcher for actions 8 | /// 9 | /// 10 | public sealed class Store where TState : class 11 | { 12 | private readonly IEnumerable> _stateFlows; 13 | private readonly List _middlewareList; 14 | private readonly List _subscribers = new(); 15 | private TState? _state; 16 | private readonly object _lockObject = new(); 17 | 18 | public Store(IEnumerable> stateFlows, IEnumerable middlewareList) 19 | { 20 | _stateFlows = stateFlows; 21 | _middlewareList = middlewareList.ToList(); 22 | } 23 | 24 | /// 25 | /// Application state 26 | /// 27 | public TState State => _state!; 28 | 29 | internal async Task Initialize(Func> func) => 30 | _state ??= await func(); 31 | 32 | /// 33 | /// Actions dispatcher 34 | /// 35 | /// Action type 36 | /// Action instance 37 | public async Task PutAsync(T action) 38 | { 39 | lock (_lockObject) 40 | { 41 | var newState = _stateFlows.Aggregate(State, (state, stateFlow) => stateFlow.Handle(state, action!)); 42 | 43 | if (!newState.Equals(State)) 44 | { 45 | _state = newState; 46 | NotifyStateSubscribers(); 47 | } 48 | } 49 | 50 | await Task.WhenAll(_middlewareList.Select(middleware => middleware.Run(action)).ToArray()); 51 | } 52 | 53 | /// 54 | /// Actions dispatcher 55 | /// 56 | /// Action type 57 | /// Action instance 58 | public void Put(T action) => 59 | Task.Run(() => PutAsync(action)); 60 | 61 | /// 62 | /// Subscribes for a state change 63 | /// 64 | /// State change callback 65 | /// Notification of state changes 66 | public Action Subscribe(Func mapper, Action notify) 67 | { 68 | var lastState = mapper(State); 69 | 70 | var subscriber = new Subscriber( 71 | oldValue: lastState, 72 | notify: notify, 73 | mapState: state => mapper(state) 74 | ); 75 | 76 | _subscribers.Add(subscriber); 77 | 78 | return () => _subscribers.Remove(subscriber); 79 | } 80 | 81 | private void NotifyStateSubscribers() 82 | { 83 | var notifiers = new List(); 84 | 85 | foreach (var subscriber in _subscribers) 86 | { 87 | var newValue = subscriber.MapState(State); 88 | if (newValue is null && subscriber.OldValue is null || newValue is not null && newValue.Equals(subscriber.OldValue)) 89 | { 90 | continue; 91 | } 92 | 93 | subscriber.OldValue = newValue; 94 | notifiers.Add(subscriber.Notify); 95 | } 96 | 97 | notifiers.Distinct().ToList().ForEach(refresh => refresh()); 98 | } 99 | 100 | private class Subscriber 101 | { 102 | public Subscriber(object? oldValue, Action notify, Func mapState) 103 | { 104 | OldValue = oldValue; 105 | Notify = notify; 106 | MapState = mapState; 107 | } 108 | 109 | public Action Notify { get; init; } 110 | public Func MapState { get; init; } 111 | public object? OldValue { get; set; } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | .idea/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # DNX 46 | project.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | # TODO: Comment the next line if you want to checkin your web deploy settings 143 | # but database connection strings (with potential passwords) will be unencrypted 144 | *.pubxml 145 | *.publishproj 146 | 147 | # Windows Azure Build Output 148 | csx/ 149 | *.build.csdef 150 | 151 | # Windows Store app package directory 152 | AppPackages/ 153 | 154 | # Visual Studio cache files 155 | # files ending in .cache can be ignored 156 | *.[Cc]ache 157 | # but keep track of directories ending in .cache 158 | !*.[Cc]ache/ 159 | 160 | # Others 161 | ClientBin/ 162 | [Ss]tyle[Cc]op.* 163 | ~$* 164 | *~ 165 | *.dbmdl 166 | *.dbproj.schemaview 167 | *.pfx 168 | *.publishsettings 169 | node_modules/ 170 | orleans.codegen.cs 171 | 172 | # RIA/Silverlight projects 173 | Generated_Code/ 174 | 175 | # Backup & report files from converting an old project file 176 | # to a newer Visual Studio version. Backup files are not needed, 177 | # because we have git ;-) 178 | _UpgradeReport_Files/ 179 | Backup*/ 180 | UpgradeLog*.XML 181 | UpgradeLog*.htm 182 | 183 | # SQL Server files 184 | *.mdf 185 | *.ldf 186 | 187 | # Business Intelligence projects 188 | *.rdl.data 189 | *.bim.layout 190 | *.bim_*.settings 191 | 192 | # Microsoft Fakes 193 | FakesAssemblies/ 194 | 195 | # Node.js Tools for Visual Studio 196 | .ntvs_analysis.dat 197 | 198 | # Visual Studio 6 build log 199 | *.plg 200 | 201 | # Visual Studio 6 workspace options file 202 | *.opt 203 | 204 | # Visual Studio LightSwitch build output 205 | **/*.HTMLClient/GeneratedArtifacts 206 | **/*.DesktopClient/GeneratedArtifacts 207 | **/*.DesktopClient/ModelManifest.xml 208 | **/*.Server/GeneratedArtifacts 209 | **/*.Server/ModelManifest.xml 210 | _Pvt_Extensions 211 | 212 | # Paket dependency manager 213 | .paket/paket.exe 214 | 215 | # FAKE - F# Make 216 | .fake/ 217 | 218 | /BuildResult/** 219 | 220 | # The packages folder can be ignored because of Package Restore 221 | **/packages/* 222 | # except commandline, which is used for restore 223 | !**/packages/NuGet.CommandLine* 224 | -------------------------------------------------------------------------------- /src/RudderOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Rudder.Middleware; 4 | 5 | namespace Rudder 6 | { 7 | /// 8 | /// Provides the configuration for Rudder library 9 | /// 10 | /// Application state type 11 | public interface IRudderOptions where TState : class 12 | { 13 | /// 14 | /// Registers a state flow in a container 15 | /// 16 | /// IStateFlow implementation 17 | IRudderOptions AddStateFlow() where T : class, IStateFlow; 18 | 19 | /// 20 | /// Registers all state flows from calling assembly and additional assemblies 21 | /// 22 | /// Application state type 23 | IRudderOptions AddStateFlows(params Assembly[] additionalAssemblies); 24 | 25 | /// 26 | /// Registers a logic flow in a container 27 | /// 28 | /// ILogicFlow implementation 29 | IRudderOptions AddLogicFlow() where T : class, ILogicFlow; 30 | 31 | /// 32 | /// Registers all logic flows from calling assembly and additional assemblies 33 | /// 34 | /// Application state type 35 | IRudderOptions AddLogicFlows(params Assembly[] additionalAssemblies); 36 | 37 | /// 38 | /// Adds JavaScript's console logging of processed actions (experimental) 39 | /// 40 | IRudderOptions AddJsLogging(); 41 | 42 | /// 43 | /// Registers a StoreMiddleware 44 | /// 45 | /// IStoreMiddleware implementation 46 | IRudderOptions AddMiddleware() where T : class, IStoreMiddleware; 47 | 48 | IRudderOptions AddStateInitializer() where T : class, IStateInitializer; 49 | } 50 | 51 | internal class RudderOptions : IRudderOptions where TState : class 52 | { 53 | private readonly IServiceCollection _services; 54 | private readonly Assembly _callingAssembly; 55 | 56 | internal RudderOptions(IServiceCollection services, Assembly callingAssembly) 57 | { 58 | _services = services; 59 | _callingAssembly = callingAssembly; 60 | } 61 | 62 | public IRudderOptions AddStateInitializer() where T : class, IStateInitializer 63 | { 64 | _services.AddScoped, T>(); 65 | 66 | return this; 67 | } 68 | 69 | public IRudderOptions AddStateFlow() where T : class, IStateFlow 70 | { 71 | _services.AddScoped, T>(); 72 | 73 | return this; 74 | } 75 | 76 | public IRudderOptions AddStateFlows(params Assembly[] additionalAssemblies) 77 | { 78 | CombineAssemblies(additionalAssemblies) 79 | .SelectMany(GetTypesOf>) 80 | .ToList() 81 | .ForEach(type => _services.AddScoped(typeof(IStateFlow), type)); 82 | 83 | return this; 84 | } 85 | 86 | public IRudderOptions AddLogicFlow() where T : class, ILogicFlow 87 | { 88 | _services.AddScoped(); 89 | 90 | return this; 91 | } 92 | 93 | public IRudderOptions AddLogicFlows(params Assembly[] additionalAssemblies) 94 | { 95 | CombineAssemblies(additionalAssemblies) 96 | .SelectMany(GetTypesOf) 97 | .ToList() 98 | .ForEach(type => _services.AddScoped(typeof(ILogicFlow), type)); 99 | 100 | return this; 101 | } 102 | 103 | public IRudderOptions AddMiddleware() where T : class, IStoreMiddleware 104 | { 105 | _services.AddScoped(); 106 | return this; 107 | } 108 | 109 | public IRudderOptions AddJsLogging() 110 | { 111 | AddMiddleware(); 112 | return this; 113 | } 114 | 115 | private IEnumerable CombineAssemblies(Assembly[] additionalAssemblies) => 116 | new[] { _callingAssembly }.Union(additionalAssemblies).ToList(); 117 | 118 | private List GetTypesOf(Assembly assembly) => 119 | assembly 120 | .GetTypes() 121 | .Where(type => typeof(T).IsAssignableFrom(type)) 122 | .ToList(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Rudder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rudder 5 | 6 | 7 | 8 | 9 | Encapsulates a method that has a single parameter and returns a Task. 10 | 11 | Parameter type 12 | Parameter value 13 | 14 | 15 | 16 | Provides handler for an action that will execute the business logic and dispatch further action if needed 17 | 18 | 19 | 20 | 21 | Handler for in incoming action for executing the business logic 22 | 23 | Action that is being currently processed 24 | 25 | 26 | 27 | Provides a handler for an action that will change the state accordingly to the processing action 28 | 29 | 30 | 31 | 32 | 33 | Handler for an action that will change the state accordingly to the processing action 34 | 35 | Current application state 36 | Processing action 37 | 38 | 39 | 40 | 41 | Provides initial state for the application 42 | 43 | 44 | 45 | 46 | 47 | Provides initial state for the application 48 | 49 | TState instance 50 | 51 | 52 | 53 | Provides a custom logic to execute when an action is being processed 54 | 55 | 56 | 57 | 58 | Logic to execute when an action is being processed 59 | 60 | Action type 61 | Action instance 62 | 63 | 64 | 65 | Provides JavaScript logging for dispatched actions in a console 66 | 67 | 68 | 69 | 70 | Provides logic flows execution 71 | 72 | 73 | 74 | 75 | Configures Rudder library 76 | 77 | Application state type 78 | Services collection 79 | Configuration options 80 | 81 | 82 | 83 | 84 | Provides the configuration for Rudder library 85 | 86 | Application state type 87 | 88 | 89 | 90 | Registers a state flow in a container 91 | 92 | IStateFlow implementation 93 | 94 | 95 | 96 | Registers all state flows from calling assembly and additional assemblies 97 | 98 | Application state type 99 | 100 | 101 | 102 | Registers a logic flow in a container 103 | 104 | ILogicFlow implementation 105 | 106 | 107 | 108 | Registers all logic flows from calling assembly and additional assemblies 109 | 110 | Application state type 111 | 112 | 113 | 114 | Adds JavaScript's console logging of processed actions (experimental) 115 | 116 | 117 | 118 | 119 | Registers a StoreMiddleware 120 | 121 | IStoreMiddleware implementation 122 | 123 | 124 | 125 | Component base class with access to the state and actions' dispatcher 126 | 127 | Application state type 128 | Component state type 129 | 130 | 131 | 132 | Dispatches an action 133 | 134 | Action type 135 | Action instance 136 | 137 | 138 | 139 | Dispatches an action 140 | 141 | Action type 142 | Action instance 143 | 144 | 145 | 146 | Action dispatched after first render of the application 147 | 148 | 149 | 150 | 151 | Provides the application state and dispatcher for actions 152 | 153 | 154 | 155 | 156 | 157 | Application state 158 | 159 | 160 | 161 | 162 | Actions dispatcher 163 | 164 | Action type 165 | Action instance 166 | 167 | 168 | 169 | Actions dispatcher 170 | 171 | Action type 172 | Action instance 173 | 174 | 175 | 176 | Subscribes for a state change 177 | 178 | State change callback 179 | Notification of state changes 180 | 181 | 182 | 183 | Component used for store initialization 184 | 185 | State type 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rudder 2 | Rudder is a state container for Blazor. 3 | 4 | The main features of Rudder: 5 | * One state object per whole application 6 | * The user interface interactions dispatch actions 7 | * Changes to the main app state are done only in the state flows. State flows are responsible for taking the dispatched actions and depending of the type and data of those actions changing the state accordingly. 8 | * Business logic is triggered in logic flows. Logic flows are responsible for taking the dispatched actions, executing business logic and optionally dispatching another actions during that process. 9 | * Works with client-side and server-side Blazor. 10 | 11 | [Example of a Blazor project using Rudder](https://github.com/kjeske/rudder-example) 12 | 13 | # Purpose 14 | Rudder makes it easier to maintain the application state and user interactions. Rudder brings: 15 | * Clean separation of concerns. State flows change the state, logic flows handle the side effects, components render the view and dispatch actions. 16 | * Easier testing 17 | * Global state makes it easier to serialize and persist in the storage for recovery purposes 18 | * Views don't contain complex logic and are responsible mostly for rendering and dispatching simple actions objects 19 | 20 | # How it works 21 | 1. There is one state object that represents the whole application UI state. 22 | 1. Application state is wrapped in a `Store` class that apart from keeping the state is also responsible for dispatching the actions. Actions are objects that describe events together with corresponding event data. 23 | 1. Views are rendered using the Razor components. In Rudder we can distinguish two types of components: 24 | - Components that have access to the `Store` - called StateComponents. 25 | - Components without the access to the `Store` - just regular components. 26 | 1. User interactions, like `onclick`, are handled in components and then can be transformed into appropriate action objects that describe the user intention (for example ReloadList action) and then dispatched. 27 | 1. Every dispatched action triggers: 28 | * State flows that are responsible for changing the state accordingly to the processing action. Example: 29 | - If currently processing action is `Actions.GetItems.Request`, then set `IsFetching` state property to `true`. 30 | * Logic flows that are responsible for handling the business logic and dispatching further actions during that process. Example: 31 | - If currently processing action is `Actions.GetItems` then: 32 | - Dispatch `Actions.GetItems.Request` action 33 | - Load items from the database 34 | - Dispatch `Actions.GetItems.Success` action with items data if the request was successful. 35 | - Dispatch `Actions.GetItems.Failure` action with error message if the request was not successful. 36 | 1. Every application state's change triggers the rerendering only of those `StateComponents` which subscribe to the changed property of the state. 37 | 38 | # Getting started with new application 39 | 40 | Rudder works with applications using Blazor, which is a part of .NET 6 41 | 42 | ## Installation 43 | Add the Rudder NuGet package to your application. 44 | 45 | Command line: 46 | ``` 47 | dotnet add package Rudder 48 | ``` 49 | Package Manager Console: 50 | ``` 51 | Install-Package Rudder 52 | ``` 53 | 54 | ## AppState 55 | We will start with creating first draft of our application's state in a class. It will be used to keep the data used by the UI. 56 | 57 | ```C# 58 | public record AppState 59 | { 60 | public IReadOnlyList Items { get; init; } 61 | public bool IsFetching { get; init; } 62 | } 63 | ``` 64 | 65 | ## Initial state 66 | We need to define our initial shape of application state. It can be used to prepare the state basing on currently logged in user and data from the database. 67 | 68 | ```C# 69 | public class AppStateInitializer : IStateInitializer 70 | { 71 | public Task GetInitialStateAsync() 72 | { 73 | var state = new AppState 74 | { 75 | Items = new[] { "Item 1" }, 76 | IsFetching = false 77 | }; 78 | 79 | return Task.FromResult(state); 80 | } 81 | } 82 | 83 | ``` 84 | 85 | ## App.razor 86 | In the Blazor app entry point (usually App.razor component) we need to wrap existing logic with StoreContainer component in order to initialize the store. 87 | 88 | ```razor 89 | 90 | 91 | 92 | 93 | ``` 94 | 95 | ## Actions 96 | 97 | Actions describe events that float thorough the system and are handled by state flows and logic flows. State flows change the state and logic flows handle the side effects and are mainly responsible for business logic. 98 | 99 | Actions are defined by records which can pass data when needed. The structure of the actions is arbitrary. 100 | 101 | ```C# 102 | public static class Actions 103 | { 104 | public record LoadItems 105 | { 106 | public record Request; 107 | public record Success(string[] Items); 108 | public record Failure(string ErrorMessage); 109 | } 110 | } 111 | ``` 112 | Remarks: 113 | * `LoadItems` action is to trigger fetching the items from some storage 114 | * `LoadItems.Request` action is to inform that the request to get the data has started 115 | * `LoadItems.Success` action is to inform that getting the data is finished. It contains Items property with retrieved data 116 | 117 | ## First state component 118 | 119 | Let's create a state component with access to the `Store` and name it `Items.razor`. It will show the elements from our state and have a button to load those elements. When the elements are being fetched, the loading indication should be shown. 120 | 121 | We will start with creating the `Items.razor` component: 122 | 123 | ```razor 124 | @inherits StateComponent 125 | 126 |

App

127 | 128 | @if (isFetching) 129 | { 130 |

Is loading

131 | } 132 | else 133 | { 134 | foreach (var item in items) 135 | { 136 |

item

137 | } 138 | 139 | 140 | } 141 | 142 | @code { 143 | string[] items; 144 | bool isFetching; 145 | 146 | void LoadItems() => Put(new Actions.LoadItems()); 147 | 148 | protected override void OnInitialized() 149 | { 150 | UseState(state => items = state.Items); 151 | UseState(state => isFetching = state.IsFetching); 152 | } 153 | } 154 | ``` 155 | 156 | ## State flows 157 | 158 | State flows are classes providing functionality to change the state accordingly to the coming actions. 159 | 160 | ```C# 161 | public class ItemsStateFlow : IStateFlow 162 | { 163 | public AppState Handle(AppState appState, object actionValue) => actionValue switch 164 | { 165 | Actions.LoadItems.Request => 166 | appState with { IsFetching = true }, 167 | 168 | Actions.LoadItems.Success action => 169 | appState with { IsFetching = false, Items = action.Items }; 170 | 171 | _ => appState 172 | }; 173 | } 174 | ``` 175 | Remarks: 176 | * State flow has to implement IStateFlow where AppState is our application state object type 177 | * Whenever action is dispatched in the system, the `Handle` method in all the state flows will be called 178 | * The `Handle` method is responsible for reacting on the actions and changing the state where needed 179 | * The appState parameter value is a read-only record and can't be mutated. Any intent to change the state should happen by using `with` statement, which creates a new copy of the state with a change. 180 | * There can be many state flows, each responsible for its own part of the state. 181 | 182 | ## Logic Flows 183 | 184 | Logic flows are used to run the side-effects and dispatch actions during that process. It's the right place to communicate with services, databases or external services. 185 | 186 | ```C# 187 | public class ItemsLogicFlow : ILogicFlow 188 | { 189 | private readonly Store _store; 190 | private readonly IAppService _appService; 191 | 192 | public ItemsFlow(Store store, IAppService appService) 193 | { 194 | _store = store; 195 | _appService = appService; 196 | } 197 | 198 | public async Task OnNext(object actionValue) 199 | { 200 | switch (actionValue) 201 | { 202 | case Actions.LoadItems: 203 | await LoadItems(); 204 | break; 205 | } 206 | } 207 | 208 | private async Task LoadItems() 209 | { 210 | _store.Put(new Actions.LoadItems.Request()); 211 | 212 | try 213 | { 214 | var result = await _appService.GetItems(); 215 | _store.Put(new Actions.LoadItems.Success { Items = result.ToArray() }); 216 | } 217 | catch (Exception exception) 218 | { 219 | _store.Put(new Actions.LoadItems.Failure { ErrorMessage = exception.Message }); 220 | } 221 | } 222 | } 223 | ``` 224 | 225 | * Logic Flows have to implement `ILogicFlow` 226 | * The asynchronous OnNext method is responsible for handling the actions and executing corresponding logic 227 | * During handling one action (LoadItems.Invoke) we can dispatch many other actions, like Request or Success that will be handled by state flows 228 | 229 | ## Startup 230 | 231 | Having all the pieces in place we can configure Rudder in the application startup. 232 | 233 | In `Startup.cs`: 234 | ```C# 235 | public void ConfigureServices(IServiceCollection services) 236 | { 237 | // ... 238 | 239 | services.AddRudder(options => 240 | { 241 | options.AddStateInitializer(); 242 | options.AddStateFlows(); 243 | options.AddLogicFlows(); 244 | }); 245 | } 246 | ``` 247 | 248 | * `options.AddStateInitializer` is used to register our state initializer that we prepared before. 249 | * `options.AddStateFlows` is used to add all the state flows from the calling assembly. Optionally we can add more assemblies to scan as param array parameter. 250 | * `options.AddLogicFlows` is used to add all the logic flows from the calling assembly. Optionally we can add more assemblies to scan as param array parameter. 251 | * It's possible to create middleware that will run after dispatching each action. Such middleware has to implement IStoreMiddleware and can be registered using `options.AddMiddleware` method. 252 | 253 | ## Logging 254 | Optionally we can add JavaScript console logging of dispatched actions. In order to do it we need to register the middleware like below. 255 | 256 | ```C# 257 | public void ConfigureServices(IServiceCollection services) 258 | { 259 | // ... 260 | 261 | services.AddRudder(options => 262 | { 263 | options.AddStateInitializer(); 264 | options.AddStateFlows(); 265 | options.AddLogicFlows(); 266 | 267 | #if DEBUG 268 | options.AddJsLogging(); // Logging middleware 269 | #endif 270 | }); 271 | } 272 | ``` 273 | 274 | Having the logging middleware enabled, we can observe the dispatched actions together with corresponding data in the web browser's console during application runtime: 275 | 276 | ![Logging](https://raw.githubusercontent.com/kjeske/rudder/master/docs/img/logging.png) 277 | 278 | ## License 279 | 280 | Rudder is Copyright © 2022 Krzysztof Jeske and other contributors under the [MIT license](https://raw.githubusercontent.com/kjeske/rudder/master/LICENSE.txt) --------------------------------------------------------------------------------