├── 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 | [](https://travis-ci.org/pshomov/reducto)
4 | [](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 |
--------------------------------------------------------------------------------