├── src ├── Reducto.Tests │ ├── packages.config │ ├── EmptyClass.cs │ ├── AsyncActions.cs │ ├── Reducto.Tests.csproj │ ├── ExampleTest.cs │ ├── StoreTests.cs │ ├── ReducersTests.cs │ └── MiddlewareTests.cs └── Reducto │ ├── .gitignore │ ├── Reducto.nuspec │ ├── Properties │ └── AssemblyInfo.cs │ ├── SimpleReducer.cs │ ├── Reducto.csproj │ ├── CompositeReducer.cs │ └── Store.cs ├── .travis.yml ├── .gitignore ├── LICENSE ├── Reducto.sln └── README.md /src/Reducto.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Reducto.Tests/EmptyClass.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Reducto.Tests 3 | { 4 | public class EmptyClass 5 | { 6 | public EmptyClass () 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | solution: Reducto.sln 3 | install: 4 | - nuget restore Reducto.sln 5 | - nuget install NUnit.Console -Version 3.5.0 -OutputDirectory testrunner 6 | script: 7 | - xbuild /p:Configuration=Release Reducto.sln 8 | - mono ./testrunner/NUnit.ConsoleRunner.3.5.0/tools/nunit3-console.exe ./src/Reducto.Tests/bin/Release/Reducto.Tests.dll -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Autosave files 2 | *~ 3 | 4 | #build 5 | [Oo]bj/ 6 | [Bb]in/ 7 | packages/ 8 | TestResults/ 9 | 10 | # globs 11 | Makefile.in 12 | *.DS_Store 13 | *.sln.cache 14 | *.suo 15 | *.cache 16 | *.pidb 17 | *.userprefs 18 | *.usertasks 19 | config.log 20 | config.make 21 | config.status 22 | aclocal.m4 23 | install-sh 24 | autom4te.cache/ 25 | *.user 26 | *.tar.gz 27 | tarballs/ 28 | test-results/ 29 | Thumbs.db 30 | 31 | #Mac bundle stuff 32 | *.dmg 33 | *.app 34 | 35 | #resharper 36 | *_Resharper.* 37 | *.Resharper 38 | 39 | #dotCover 40 | *.dotCover 41 | -------------------------------------------------------------------------------- /src/Reducto/.gitignore: -------------------------------------------------------------------------------- 1 | #Autosave files 2 | *~ 3 | 4 | #build 5 | [Oo]bj/ 6 | [Bb]in/ 7 | packages/ 8 | TestResults/ 9 | 10 | # globs 11 | Makefile.in 12 | *.DS_Store 13 | *.sln.cache 14 | *.suo 15 | *.cache 16 | *.pidb 17 | *.userprefs 18 | *.usertasks 19 | config.log 20 | config.make 21 | config.status 22 | aclocal.m4 23 | install-sh 24 | autom4te.cache/ 25 | *.user 26 | *.tar.gz 27 | tarballs/ 28 | test-results/ 29 | Thumbs.db 30 | 31 | #Mac bundle stuff 32 | *.dmg 33 | *.app 34 | 35 | #resharper 36 | *_Resharper.* 37 | *.Resharper 38 | 39 | #dotCover 40 | *.dotCover 41 | -------------------------------------------------------------------------------- /src/Reducto/Reducto.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reducto 5 | 0.9.2 6 | Petar Shomov 7 | Petar Shomov 8 | http://www.opensource.org/licenses/mit-license.php 9 | https://github.com/pshomov/reducto 10 | 11 | false 12 | A port of Redux to .NET 13 | Getting rid of the Action interface and removing contraints on the type of the state 14 | Copyright 2015 15 | redux mvvm mvc pattern 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Reducto/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | 7 | [assembly: AssemblyTitle ("Reducto")] 8 | [assembly: AssemblyDescription ("")] 9 | [assembly: AssemblyConfiguration ("")] 10 | [assembly: AssemblyCompany ("")] 11 | [assembly: AssemblyProduct ("")] 12 | [assembly: AssemblyCopyright ("petar")] 13 | [assembly: AssemblyTrademark ("")] 14 | [assembly: AssemblyCulture ("")] 15 | 16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 19 | 20 | [assembly: AssemblyVersion ("0.9.*")] 21 | 22 | // The following attributes are used to specify the signing key for the assembly, 23 | // if desired. See the Mono documentation for more information about signing. 24 | 25 | //[assembly: AssemblyDelaySign(false)] 26 | //[assembly: AssemblyKeyFile("")] 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Petar Shomov 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 | -------------------------------------------------------------------------------- /Reducto.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reducto", "src\Reducto\Reducto.csproj", "{791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reducto.Tests", "src\Reducto.Tests\Reducto.Tests.csproj", "{74FEF20E-0AC9-431A-A8FA-178AFFA678B3}" 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 | {74FEF20E-0AC9-431A-A8FA-178AFFA678B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {74FEF20E-0AC9-431A-A8FA-178AFFA678B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {74FEF20E-0AC9-431A-A8FA-178AFFA678B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {74FEF20E-0AC9-431A-A8FA-178AFFA678B3}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /src/Reducto/SimpleReducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Reducto 5 | { 6 | public class SimpleReducer 7 | { 8 | private readonly Dictionary handlers = new Dictionary(); 9 | private readonly Func stateInitializer; 10 | 11 | public SimpleReducer() 12 | { 13 | stateInitializer = () => default(State); 14 | } 15 | 16 | public SimpleReducer(Func initializer) 17 | { 18 | this.stateInitializer = initializer; 19 | } 20 | 21 | public SimpleReducer When(Func handler) 22 | { 23 | handlers.Add(typeof (Event), handler); 24 | return this; 25 | } 26 | 27 | public Reducer Get() 28 | { 29 | return delegate(State state, Object action) 30 | { 31 | var prevState = action.GetType() == typeof (InitStoreAction) ? stateInitializer() : state; 32 | if (handlers.ContainsKey(action.GetType())) 33 | { 34 | var handler = handlers[action.GetType()]; 35 | return (State) handler.DynamicInvoke(prevState, action); 36 | } 37 | return prevState; 38 | }; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Reducto/Reducto.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD} 8 | Library 9 | Reducto 10 | Reducto 11 | v4.5 12 | Profile78 13 | 14 | 15 | true 16 | full 17 | false 18 | bin\Debug 19 | DEBUG; 20 | prompt 21 | 4 22 | false 23 | 24 | 25 | full 26 | true 27 | bin\Release 28 | prompt 29 | 4 30 | false 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Reducto.Tests/AsyncActions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace Reducto.Tests 6 | { 7 | public struct LoginInfo 8 | { 9 | public string username; 10 | } 11 | 12 | [TestFixture] 13 | public class AsyncActions 14 | { 15 | [Test] 16 | public async Task should_allow_for_async_execution_of_code() 17 | { 18 | var storeReducerReached = 0; 19 | var reducer = new SimpleReducer>(() => new List {"a"}).When((s, e) => 20 | { 21 | storeReducerReached += 1; 22 | return s; 23 | }); 24 | var store = new Store>(reducer); 25 | 26 | var result = await store.Dispatch(store.asyncAction(async (dispatcher, store2) => 27 | { 28 | await Task.Delay(300); 29 | Assert.That(store2()[0], Is.EqualTo("a")); 30 | dispatcher(new SomeAction()); 31 | return 112; 32 | })); 33 | 34 | Assert.That(storeReducerReached, Is.EqualTo(1)); 35 | Assert.That(result, Is.EqualTo(112)); 36 | } 37 | 38 | [Test] 39 | public async Task should_allow_for_passing_parameters_to_async_actions() 40 | { 41 | var storeReducerReached = 0; 42 | var reducer = new SimpleReducer>(() => new List {"a"}).When((s, e) => 43 | { 44 | storeReducerReached += 1; 45 | return s; 46 | }); 47 | var store = new Store>(reducer); 48 | 49 | var action1 = store.asyncAction(async (dispatcher, store2, msg) => 50 | { 51 | await Task.Delay(300); 52 | Assert.That(msg.username, Is.EqualTo("John")); 53 | dispatcher(new SomeAction()); 54 | return 112; 55 | }); 56 | var result = await store.Dispatch(action1(new LoginInfo 57 | { 58 | username = "John" 59 | })); 60 | 61 | Assert.That(storeReducerReached, Is.EqualTo(1)); 62 | Assert.That(result, Is.EqualTo(112)); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Reducto.Tests/Reducto.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {74FEF20E-0AC9-431A-A8FA-178AFFA678B3} 7 | Library 8 | Reducto.Tests 9 | Reducto.Tests 10 | v4.5.1 11 | 12 | 13 | 14 | true 15 | full 16 | false 17 | bin\Debug 18 | DEBUG; 19 | prompt 20 | 4 21 | false 22 | 23 | 24 | full 25 | true 26 | bin\Release 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | 33 | 34 | ..\..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {791678FE-0CF1-4A84-BCBD-EB6DBBAB10CD} 49 | Reducto 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/Reducto.Tests/ExampleTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using System.Threading.Tasks; 4 | 5 | namespace Reducto.Tests 6 | { 7 | [TestFixture] 8 | public class ExampleTest 9 | { 10 | // Actions 11 | 12 | public struct LoginStarted 13 | { 14 | public string Username; 15 | } 16 | 17 | public struct LoginFailed {} 18 | 19 | public struct LoginSucceeded 20 | { 21 | public string Token; 22 | } 23 | 24 | // State 25 | 26 | public enum LoginStatus 27 | { 28 | LoginInProgress, LoggedIn, NotLoggedIn 29 | } 30 | 31 | public struct AppState 32 | { 33 | public LoginStatus Status; 34 | public String Username; 35 | public String Token; 36 | } 37 | 38 | [Test] 39 | public async Task all_in_one_example(){ 40 | var reducer = new SimpleReducer() 41 | .When((state, action) => { 42 | state.Username = action.Username; 43 | state.Token = ""; 44 | state.Status = LoginStatus.LoginInProgress; 45 | return state; 46 | }) 47 | .When((state, action) => { 48 | state.Token = action.Token; 49 | state.Status = LoginStatus.LoggedIn; 50 | return state; 51 | }) 52 | .When((state, action) => { 53 | state.Status = LoginStatus.NotLoggedIn; 54 | return state; 55 | }); 56 | 57 | var store = new Store(reducer); 58 | 59 | var loginAsyncAction = store.asyncAction(async(dispatch, getState) => { 60 | dispatch(new LoginStarted{Username = "John Doe"}); 61 | 62 | // faking authentication of user 63 | await Task.Delay(500); 64 | var authenticated = new Random().Next() % 2 == 0; 65 | 66 | if (authenticated) { 67 | dispatch(new LoginSucceeded{Token = "1234"}); 68 | } else { 69 | dispatch(new LoginFailed()); 70 | } 71 | return authenticated; 72 | }); 73 | 74 | var logged = await store.Dispatch(loginAsyncAction); 75 | 76 | if (logged){ 77 | Assert.That(store.GetState().Status, Is.EqualTo(LoginStatus.LoggedIn)); 78 | } else { 79 | Assert.That(store.GetState().Status, Is.EqualTo(LoginStatus.NotLoggedIn)); 80 | } 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/Reducto/CompositeReducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | 6 | namespace Reducto 7 | { 8 | public class CompositeReducer 9 | { 10 | private readonly List> fieldReducers = new List>(); 11 | private readonly Func stateInitializer; 12 | 13 | public CompositeReducer() 14 | { 15 | stateInitializer = () => default(State); 16 | } 17 | 18 | public CompositeReducer(Func initializer) 19 | { 20 | this.stateInitializer = initializer; 21 | } 22 | 23 | public CompositeReducer Part(Expression> composer, SimpleReducer reducer) 24 | { 25 | return Part(composer, reducer.Get()); 26 | } 27 | 28 | public CompositeReducer Part(Expression> composer, CompositeReducer reducer) 29 | { 30 | return Part(composer, reducer.Get()); 31 | } 32 | 33 | public CompositeReducer Part(Expression> composer, Reducer reducer) 34 | { 35 | var memberExpr = composer.Body as MemberExpression; 36 | var member = (FieldInfo) memberExpr.Member; 37 | 38 | if (memberExpr == null) 39 | throw new ArgumentException(string.Format( 40 | "Expression '{0}' should be a field.", 41 | composer.ToString())); 42 | if (member == null) 43 | throw new ArgumentException(string.Format( 44 | "Expression '{0}' should be a constant expression", 45 | composer.ToString())); 46 | 47 | fieldReducers.Add(new Tuple(member, reducer)); 48 | return this; 49 | } 50 | 51 | public Reducer Get() 52 | { 53 | return delegate(State state, Object action) 54 | { 55 | var result = action.GetType() == typeof (InitStoreAction) ? stateInitializer() : state; 56 | foreach (var fieldReducer in fieldReducers) 57 | { 58 | var prevState = action.GetType() == typeof (InitStoreAction) 59 | ? null 60 | : fieldReducer.Item1.GetValue(state); 61 | var newState = fieldReducer.Item2.DynamicInvoke(prevState, action); 62 | object boxer = result; //boxing to allow the next line work for both reference and value objects 63 | fieldReducer.Item1.SetValue(boxer, newState); 64 | result = (State) boxer; // unbox, hopefully not too much performance penalty 65 | } 66 | return result; 67 | }; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Reducto.Tests/StoreTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using System; 4 | 5 | namespace Reducto.Tests 6 | { 7 | public class ItemAdded 8 | { 9 | public string item; 10 | } 11 | 12 | [TestFixture] 13 | public class StoreTests 14 | { 15 | [Test] 16 | public void should_notify_subscribers_while_they_are_subscribed() 17 | { 18 | var reducer = new SimpleReducer>(() => new List {"Use ReduxVVM"}); 19 | var store = new Store>(reducer); 20 | 21 | var changed = 0; 22 | var unsub = store.Subscribe(state => 23 | { 24 | Assert.NotNull(state); 25 | changed += 1; 26 | }); 27 | 28 | store.Dispatch(new ItemAdded {item = "Read the Redux docs"}); 29 | store.Dispatch(new ItemAdded {item = "Read the Redux docs"}); 30 | 31 | Assert.That(changed, Is.EqualTo(2)); 32 | unsub(); 33 | store.Dispatch(new ItemAdded {item = ""}); 34 | 35 | Assert.That(changed, Is.EqualTo(2)); 36 | } 37 | 38 | [Test] 39 | public void should_register_root_reducer() 40 | { 41 | Reducer> reducer = (List state, Object action) => 42 | { 43 | if (action.GetType() == typeof (InitStoreAction)) return new List {"Use ReduxVVM"}; 44 | 45 | var newState = new List(state); 46 | 47 | switch (action.GetType().Name) 48 | { 49 | case "ItemAdded": 50 | var concreteEv = (ItemAdded) action; 51 | newState.Add(concreteEv.item); 52 | break; 53 | default: 54 | break; 55 | } 56 | return newState; 57 | }; 58 | var store = new Store>(reducer); 59 | store.Dispatch(new ItemAdded {item = "Read the Redux docs"}); 60 | 61 | CollectionAssert.AreEqual(store.GetState(), new List {"Use ReduxVVM", "Read the Redux docs"}); 62 | } 63 | 64 | [Test] 65 | public void should_register_root_reducer_with_builder() 66 | { 67 | var reducer = new SimpleReducer>(() => new List {"Use ReduxVVM"}) 68 | .When((state, action) => 69 | { 70 | var newSatte = new List(state); 71 | newSatte.Add(action.item); 72 | return newSatte; 73 | }) 74 | .Get(); 75 | var store = new Store>(reducer); 76 | store.Dispatch(new ItemAdded {item = "Read the Redux docs"}); 77 | 78 | CollectionAssert.AreEqual(store.GetState(), new List {"Use ReduxVVM", "Read the Redux docs"}); 79 | } 80 | 81 | [Test] 82 | public void should_return_same_state_when_command_not_for_that_reducer() 83 | { 84 | var reducer = new SimpleReducer>(() => new List {"Use ReduxVVM"}); 85 | var store = new Store>(reducer); 86 | store.Dispatch(new ItemAdded {item = "Read the Redux docs"}); 87 | 88 | CollectionAssert.AreEqual(store.GetState(), new List {"Use ReduxVVM"}); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/Reducto.Tests/ReducersTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Reducto.Tests 4 | { 5 | public class TopicSet 6 | { 7 | public string topic; 8 | } 9 | 10 | public class FilterVisibility 11 | { 12 | public bool visible; 13 | } 14 | 15 | public struct AppStore 16 | { 17 | public string redditTopic; 18 | public bool visibility; 19 | 20 | public override string ToString() 21 | { 22 | return string.Format("topic:{0}, visibility {1}", redditTopic, visibility); 23 | } 24 | } 25 | 26 | [TestFixture] 27 | public class ReducersTests 28 | { 29 | private struct Address 30 | { 31 | public string city; 32 | public string streetNr; 33 | } 34 | 35 | private enum DeliveryMethod 36 | { 37 | REGULAR, 38 | GUARANTEED 39 | } 40 | 41 | private struct Destination 42 | { 43 | public Address addr; 44 | public DeliveryMethod deliver; 45 | } 46 | 47 | private struct Order 48 | { 49 | public Destination destination; 50 | public string name; 51 | public Address origin; 52 | } 53 | 54 | private struct SetOrigin 55 | { 56 | public Address newAddress; 57 | } 58 | 59 | private struct SetDestination 60 | { 61 | public Address newAddress; 62 | } 63 | 64 | private struct BehindSchedule 65 | { 66 | } 67 | 68 | private struct SetDelivery 69 | { 70 | public DeliveryMethod method; 71 | } 72 | 73 | [Test] 74 | public void should_prvide_way_to_combine_reducers() 75 | { 76 | var topicReducer = new SimpleReducer().When((s, e) => e.topic); 77 | var visibilityReducer = new SimpleReducer().When((s, e) => e.visible); 78 | var reducer = new CompositeReducer(() => new AppStore {redditTopic = "react", visibility = false}) 79 | .Part(state => state.redditTopic, topicReducer) 80 | .Part(state => state.visibility, visibilityReducer); 81 | var store = new Store(reducer); 82 | store.Dispatch(new TopicSet {topic = "Redux is awesome"}); 83 | store.Dispatch(new FilterVisibility {visible = true}); 84 | 85 | Assert.AreEqual(new AppStore {redditTopic = "Redux is awesome", visibility = true}, store.GetState()); 86 | } 87 | 88 | [Test] 89 | public void should_prvide_way_to_create_deep_hierarchy_of_reducers() 90 | { 91 | var originReducer = new SimpleReducer
().When((s, e) => e.newAddress); 92 | var destinationReducer = new CompositeReducer() 93 | .Part(state => state.deliver, 94 | new SimpleReducer().When((s, a) => DeliveryMethod.REGULAR) 95 | .When((_, a) => a.method)) 96 | .Part(state => state.addr, new SimpleReducer
().When((s, a) => a.newAddress)); 97 | var orderReducer = new CompositeReducer() 98 | .Part(state => state.origin, originReducer) 99 | .Part(state => state.destination, destinationReducer); 100 | var store = new Store(orderReducer); 101 | store.Dispatch(new SetOrigin {newAddress = new Address {streetNr = "Laugavegur 26", city = "Reykjavík"}}); 102 | store.Dispatch(new SetDestination {newAddress = new Address {streetNr = "5th Avenue", city = "New York"}}); 103 | store.Dispatch(new SetDelivery {method = DeliveryMethod.GUARANTEED}); 104 | 105 | store.Dispatch(new BehindSchedule()); 106 | 107 | Assert.AreEqual(new Order 108 | { 109 | origin = new Address {streetNr = "Laugavegur 26", city = "Reykjavík"}, 110 | destination = 111 | new Destination 112 | { 113 | addr = new Address {streetNr = "5th Avenue", city = "New York"}, 114 | deliver = DeliveryMethod.REGULAR 115 | } 116 | }, store.GetState()); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/Reducto.Tests/MiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | 4 | namespace Reducto.Tests 5 | { 6 | public class SomeAction 7 | { 8 | public string topic; 9 | } 10 | 11 | [TestFixture] 12 | public class MiddlewareTests 13 | { 14 | [Test] 15 | public void should_allow_middleware_shortcut_the_store_dispatcher() 16 | { 17 | var storeReducerReached = 0; 18 | var reducer = new SimpleReducer>().When((s, e) => 19 | { 20 | storeReducerReached += 1; 21 | return s; 22 | }); 23 | var stateStore = new Store>(reducer); 24 | var middlewareCounter = 0; 25 | stateStore.Middleware( 26 | store => next => action => 27 | { 28 | middlewareCounter += 3; 29 | Assert.That(middlewareCounter, Is.EqualTo(3)); 30 | next(action); 31 | middlewareCounter += 3000; 32 | Assert.That(middlewareCounter, Is.EqualTo(3333)); 33 | }, 34 | store => next => action => 35 | { 36 | middlewareCounter += 30; 37 | Assert.That(middlewareCounter, Is.EqualTo(33)); 38 | middlewareCounter += 300; 39 | Assert.That(middlewareCounter, Is.EqualTo(333)); 40 | } 41 | ); 42 | 43 | stateStore.Dispatch(new SomeAction()); 44 | Assert.That(middlewareCounter, Is.EqualTo(3333)); 45 | Assert.That(storeReducerReached, Is.EqualTo(0)); 46 | } 47 | 48 | [Test] 49 | public void should_allow_middleware_to_hook_into_dispatching() 50 | { 51 | var storeReducerReached = 0; 52 | var reducer = new SimpleReducer>().When((s, e) => 53 | { 54 | storeReducerReached += 1; 55 | return s; 56 | }); 57 | var stateStore = new Store>(reducer); 58 | var middlewareCounter = 0; 59 | stateStore.Middleware( 60 | store => next => action => 61 | { 62 | middlewareCounter += 3; 63 | Assert.That(middlewareCounter, Is.EqualTo(3)); 64 | next(action); 65 | middlewareCounter += 3000; 66 | Assert.That(middlewareCounter, Is.EqualTo(3333)); 67 | }, 68 | store => next => action => 69 | { 70 | middlewareCounter += 30; 71 | Assert.That(middlewareCounter, Is.EqualTo(33)); 72 | Assert.That(storeReducerReached, Is.EqualTo(0)); 73 | next(action); 74 | Assert.That(storeReducerReached, Is.EqualTo(1)); 75 | middlewareCounter += 300; 76 | Assert.That(middlewareCounter, Is.EqualTo(333)); 77 | } 78 | ); 79 | 80 | stateStore.Dispatch(new SomeAction()); 81 | Assert.That(middlewareCounter, Is.EqualTo(3333)); 82 | Assert.That(storeReducerReached, Is.EqualTo(1)); 83 | } 84 | 85 | [Test] 86 | public void should_allow_middleware_to_shortcut_lower_middleware() 87 | { 88 | var storeReducerReached = 0; 89 | var reducer = new SimpleReducer>().When((s, e) => 90 | { 91 | storeReducerReached += 1; 92 | return s; 93 | }); 94 | var stateStore = new Store>(reducer); 95 | var middlewareCounter = 0; 96 | stateStore.Middleware( 97 | store => next => action => 98 | { 99 | middlewareCounter += 3; 100 | Assert.That(middlewareCounter, Is.EqualTo(3)); 101 | middlewareCounter += 3000; 102 | Assert.That(middlewareCounter, Is.EqualTo(3003)); 103 | }, 104 | store => next => action => 105 | { 106 | middlewareCounter += 30; 107 | Assert.That(middlewareCounter, Is.EqualTo(33)); 108 | Assert.That(storeReducerReached, Is.EqualTo(0)); 109 | next(action); 110 | Assert.That(storeReducerReached, Is.EqualTo(1)); 111 | middlewareCounter += 300; 112 | Assert.That(middlewareCounter, Is.EqualTo(333)); 113 | } 114 | ); 115 | 116 | stateStore.Dispatch(new SomeAction()); 117 | Assert.That(middlewareCounter, Is.EqualTo(3003)); 118 | Assert.That(storeReducerReached, Is.EqualTo(0)); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Reducto/Store.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Reducto 7 | { 8 | public sealed class InitStoreAction 9 | { 10 | } 11 | 12 | public delegate State Reducer(State state, Object action); 13 | public delegate void StateChangedSubscriber(State state); 14 | public delegate void Unsubscribe(); 15 | public delegate void DispatcherDelegate(Object a); 16 | 17 | public interface IBasicStore 18 | { 19 | Unsubscribe Subscribe(StateChangedSubscriber subscription); 20 | void Dispatch(Object action); 21 | State GetState(); 22 | } 23 | 24 | public class Store 25 | { 26 | public delegate State GetStateDelegate(); 27 | public delegate Task AsyncAction(DispatcherDelegate dispatcher, GetStateDelegate getState); 28 | public delegate Task AsyncAction(DispatcherDelegate dispatcher, GetStateDelegate getState); 29 | public delegate AsyncAction AsyncActionNeedsParam(T param); 30 | public delegate AsyncAction AsyncActionNeedsParam(T param); 31 | 32 | 33 | private readonly BasicStore store; 34 | private MiddlewareExecutor middlewares; 35 | 36 | public Store(SimpleReducer rootReducer) : this(rootReducer.Get()) 37 | { 38 | } 39 | 40 | public Store(CompositeReducer rootReducer) : this(rootReducer.Get()) 41 | { 42 | } 43 | 44 | public Store(Reducer rootReducer) 45 | { 46 | store = new BasicStore(rootReducer); 47 | Middleware(); 48 | } 49 | 50 | public Unsubscribe Subscribe(StateChangedSubscriber subscription) 51 | { 52 | return store.Subscribe(subscription); 53 | } 54 | 55 | public void Dispatch(Object action) 56 | { 57 | middlewares(action); 58 | } 59 | 60 | public Task Dispatch(AsyncAction action) 61 | { 62 | return action(Dispatch, GetState); 63 | } 64 | 65 | public Task Dispatch(AsyncAction action) 66 | { 67 | return action(Dispatch, GetState); 68 | } 69 | 70 | public AsyncActionNeedsParam asyncAction( 71 | Func> action) 72 | { 73 | return invokeParam => (dispatch, getState) => action(dispatch, getState, invokeParam); 74 | } 75 | 76 | public AsyncActionNeedsParam asyncActionVoid( 77 | Func action) 78 | { 79 | return invokeParam => (dispatch, getState) => action(dispatch, getState, invokeParam); 80 | } 81 | 82 | public AsyncAction asyncAction( 83 | AsyncAction action) 84 | { 85 | return (dispatch, getState) => action(dispatch, getState); 86 | } 87 | 88 | public State GetState() 89 | { 90 | return store.GetState(); 91 | } 92 | 93 | public void Middleware(params Middleware[] middlewares) 94 | { 95 | this.middlewares = 96 | middlewares.Select(m => m(store)) 97 | .Reverse() 98 | .Aggregate(store.Dispatch, (acc, middle) => middle(acc)); 99 | } 100 | 101 | private class BasicStore : IBasicStore 102 | { 103 | private readonly Reducer rootReducer; 104 | 105 | private readonly List> subscriptions = 106 | new List>(); 107 | 108 | private State state; 109 | 110 | public BasicStore(Reducer rootReducer) 111 | { 112 | this.rootReducer = rootReducer; 113 | state = rootReducer(state, new InitStoreAction()); 114 | } 115 | 116 | public Unsubscribe Subscribe(StateChangedSubscriber subscription) 117 | { 118 | subscriptions.Add(subscription); 119 | return () => { subscriptions.Remove(subscription); }; 120 | } 121 | 122 | public void Dispatch(Object action) 123 | { 124 | state = rootReducer(state, action); 125 | foreach (var subscribtion in subscriptions) 126 | { 127 | subscribtion(state); 128 | } 129 | } 130 | 131 | public State GetState() 132 | { 133 | return state; 134 | } 135 | } 136 | } 137 | 138 | public delegate void MiddlewareExecutor(Object action); 139 | public delegate MiddlewareExecutor MiddlewareChainer(MiddlewareExecutor nextMiddleware); 140 | public delegate MiddlewareChainer Middleware(IBasicStore store); 141 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reducto is a port of [Redux](http://rackt.github.io/redux/) to .NET 2 | 3 | [![Build Status](https://img.shields.io/travis/pshomov/reducto.svg?style=flat-square)](https://travis-ci.org/pshomov/reducto) 4 | [![NuGet Release](https://img.shields.io/nuget/v/Reducto.svg?style=flat-square)](https://www.nuget.org/packages/Reducto/) 5 | 6 | ## What is Reducto? 7 | 8 | Reducto is a keeper of the state for your app. It helps to organize the logic that changes that state. Really useful for GUI apps in combination with(but not limited to) MVVM, MVC, MVP etc. 9 | 10 | |Metric|Value| 11 | |-------|-----| 12 | |lines of code| ~260| 13 | |dependencies| 0 | 14 | |packaging | [NuGet PCL](https://www.nuget.org/packages/Reducto/) | 15 | 16 | ## Installation 17 | 18 | In Package Manager Console run 19 | 20 | ``` 21 | PM> Install-Package Reducto 22 | ``` 23 | 24 | ## Key concepts 25 | 26 | - **Action** - an object which describes what has happened - LoggedIn, SignedOut, etc. The object contains all the information relevant to the action - username, password, status, etc. Usually there are many actions in an app. 27 | - **Reducer** - a side-effect free function that receives the current state of your app and an `action`. If the reducer does not know how to handle the `action` it should return the state as is. If the reducer can handle the `action` it 1.) makes a copy of the state 2.) it modifies it in response to the `action` and 3.) returns the copy. 28 | - **Store** - it is an object that contains your app's state. It also has a `reducer`. We _dispatch_ an `action` to the `store` which hands it to the `reducer` together with the current app state and then uses the return value of the `reducer` as the new state of the app. There is only one `store` in your app. It's created when your app starts and gets destroyed when your app quits. Your MVVM view models can _subscribe_ to be notified when the state changes so they can update themselves accordingly. 29 | - **Async action** - a function that may have side effects. This is where you talk to your database, call a web service, navigate to a view model, etc. `Async actions` can also dispatch `actions` (as described above). To execute an `async action` it needs to be _dispatched_ to the `store`. 30 | - **Middleware** - these are functions that can be hooked in the `store` dispatch mechanism so you can do things like logging, profiling, authorization, etc. It's sort of a plugin mechanism which can be quite useful. 31 | 32 | Dispatching an `action` to the store is **the only way to change its state**.
33 | Dispatching an `async action` cannot change the state but it can dispatch `actions` which in turn can change the state. 34 | 35 | ## How does one use this thing? 36 | 37 | Here is a short example of Reducto in action. Let's write an app that authenticates a user. 38 | 39 | First, let's define the `actions` that we will need: 40 | 41 | ```c# 42 | // Actions 43 | 44 | public struct LoginStarted 45 | { 46 | public string Username; 47 | } 48 | 49 | public struct LoginFailed {} 50 | 51 | public struct LoginSucceeded 52 | { 53 | public string Token; 54 | } 55 | ``` 56 | Next is the state of our app 57 | 58 | ```c# 59 | // State 60 | 61 | public enum LoginStatus 62 | { 63 | LoginInProgress, LoggedIn, NotLoggedIn 64 | } 65 | 66 | public struct AppState 67 | { 68 | public LoginStatus Status; 69 | public String Username; 70 | public String Token; 71 | } 72 | ``` 73 | 74 | Here is how the `actions` change the state of the app 75 | 76 | ```c# 77 | var reducer = new SimpleReducer() 78 | .When((state, action) => { 79 | state.Username = action.Username; 80 | state.Token = ""; 81 | state.Status = LoginStatus.LoginInProgress; 82 | return state; 83 | }) 84 | .When((state, action) => { 85 | state.Token = action.Token; 86 | state.Status = LoginStatus.LoggedIn; 87 | return state; 88 | }) 89 | .When((state, action) => { 90 | state.Status = LoginStatus.NotLoggedIn; 91 | return state; 92 | }); 93 | 94 | var store = new Store(reducer); 95 | ``` 96 | Now let's take a moment to see what is going on here. We made a `reducer` using a builder and define how each `action` changes the state. This `reducer` is provieded to the `store` so the store can use it whenever an `action` is dispatched to it. Makes sense so far? I hope so ;) 97 | 98 | Now let's see what is dispatching `actions` to the `store`. One can do that directly but more often then not it will be done from inside an `async action` like this one 99 | ```c# 100 | var loginAsyncAction = store.asyncAction(async(dispatch, getState) => { 101 | dispatch(new LoginStarted{Username = "John Doe"}); 102 | 103 | // faking authentication of user 104 | await Task.Delay(500); 105 | var authenticated = new Random().Next() % 2 == 0; 106 | 107 | if (authenticated) { 108 | dispatch(new LoginSucceeded{Token = "1234"}); 109 | } else { 110 | dispatch(new LoginFailed()); 111 | } 112 | return authenticated; 113 | }); 114 | ``` 115 | A lot going on here. The `async action` gets a _dispatch_ and a _getState_ delegates. The latter one is not used in our case but the former is used a lot. We dispatch an action to signal the login process has started and then again after it has finished and depending on the outcome of the operation. How do we use this `async action`? 116 | ```c# 117 | store.Dispatch(loginAsyncAction); 118 | // or if you need to know the result of the login you can do also 119 | var logged = await store.Dispatch(loginAsyncAction); 120 | ``` 121 | 122 | For more examples and please checkout the links below in the Resources section 123 | 124 | ## Resources 125 | 126 | A couple of links on my blog 127 | 128 | - [Better MVVM with Xamarin Forms](http://pshomov.github.io/better-mvvm-with-xamarin-forms/) 129 | - [Compartmentalizing logic](http://pshomov.github.io/compartmentalizing-logic/) 130 | 131 | ## What about the name? 132 | 133 | [It is pure magic ;-)](https://en.wikibooks.org/wiki/Muggles%27_Guide_to_Harry_Potter/Magic/Reducto#Overview) 134 | --------------------------------------------------------------------------------