Slicey.Net
1.0.0
dotnet add package Slicey.Net --version 1.0.0
NuGet\Install-Package Slicey.Net -Version 1.0.0
<PackageReference Include="Slicey.Net" Version="1.0.0" />
<PackageVersion Include="Slicey.Net" Version="1.0.0" />
<PackageReference Include="Slicey.Net" />
paket add Slicey.Net --version 1.0.0
#r "nuget: Slicey.Net, 1.0.0"
#:package Slicey.Net@1.0.0
#addin nuget:?package=Slicey.Net&version=1.0.0
#tool nuget:?package=Slicey.Net&version=1.0.0
Slicey.Net: strongly-typed Expression-based Redux-like library for .NET
About
Slicey.Net is an NgRx and Redux inspired library for state management in .NET.
It has been developed with 4 important design principles:
- It takes advantage of native C# concepts like ExpressionTrees for strong typing and
events for notifying that state has been updated - Like NgRx store, it manges state in slices so that store can be separated
in multiple easy to manage stores responsible for their own part of the state - It requires only state stores to inherit from library types, while actions, reducers
and selectors are easily added with provided builder methods - Dependency injection is supported out of the box
How to use Slicey.Net
Implementing the root store
Suppose that your application has to manage state consisting of the following class:
class AppState
{
public int IntProperty { get; set; }
public string StringProperty { get; set; } = "";
}
Suppose further that there are three types of mutations possible:
IntProperty can be increased or reset, while StringProperty
can only be appended to. Note that we are not interested in the
values of the properties themselves but in double the value of
IntPropertyand HTML formatted value of StringProperty. This
can be achieved with the following store:
public class AppStateStore : RootStateStore<AppState>
{
public AppStateStore(AppState initialState) : base(initialState)
{
DoubleIntSelector = AddSelector(state => state.IntProperty * 2);
BoldString = AddSelector(state => $"<emph> {state.StringProperty} </emph>");
IncreaseInt = AddAction<int>();
ResetInt = AddAction();
AppendToString = AddAction<string>();
AddReducer(IncreaseInt,
state => state.IntProperty,
(state, incAmount) => state.IntProperty + incAmount);
AddReducer(ResetInt,
state => state.IntProperty,
(_) => 0);
AddReducer(AppendToString,
state => state.StringProperty,
(state, suffix) => state.StringProperty + suffix);
}
public Selector<AppState, int> DoubleIntSelector { get; }
public Selector<AppState, string> BoldString { get; }
public StateAction<int> IncreaseInt { get; }
public StateAction ResetInt { get; }
public StateAction<string> AppendToString { get; }
}
- Selectors are simply defined using builder method and
expression that produces a value using the state varibles.
Their current value can be read by converting the selector
to the target type of selector (e.g.
(string)BoldString). They also expose eventSelectorUpdatedwhenever new value is generated for selector. - Actions can have arguments like
IncreaseIntandAppendToStringor have no arguments likeResetInt - Reducers represent rules that describe how the state is updated
when action occurs. For example, the first reducer defined above states
that propery
IntPropertyis incremented by action argumentincAmount - Actions are dispatched by calling the
Dispatchmethod on the store. For example, to increase theIntPropertyby 5 we callstore.Dispatch(store.IncreaseInt, 5). To reset the same property, we callstore.Dispatch(store.ResetInt); This will dispatch an action that will, according to the rules described by reducers update the state and invoke the selectors that have been updated. - Note that selectors and actions are exposed as public properties as they are
used outside the store, while reducers are internal rules on how to update the state and are not exposed.
Slicing the state
Now suppose that we have two independent modules that maintain their own state
and a root module that is simply interested in the state of both modules. This can be
best modelled by SliceStores:
public class ModuleAState
{
public string PropA { get; set; } = "";
}
public class ModuleBState
{
public int PropB { get; set; }
}
public class AppState
{
public ModuleAState ModuleAState { get; set; } = new();
public ModuleBState ModuleBState { get; set; } = new();
}
public class AppStateStore : RootStateStore<AppState>
{
public AppStateStore(AppState initialState) : base(initialState)
{
AState = AddSelector(state => state.ModuleAState.PropA);
BState = AddSelector(state => state.ModuleBState.PropB);
}
public Selector<AppState, string> AState { get; }
public Selector<AppState, int> BState { get; }
}
public class ModuleAStore : SliceStateStore<AppState, ModuleAState>
{
public ModuleAStore(RootStateStore<AppState> rootStore) : base(rootStore, store => store.ModuleAState)
{
// Actions, Selectors and Reducers that are scoped to ModuleAState
}
}
public class ModuleBStore : SliceStateStore<AppState, ModuleBState>
{
public ModuleBStore(RootStateStore<AppState> rootStore) : base(rootStore, store => store.ModuleBState)
{
// Actions, Selectors and Reducers that are scoped to ModuleAState
}
}
Mutability
Unlike JavaScript based state stores, Slicey.NET does not require that every reducer creates a new copy of the state. This can lead to unwanted side effects if the objects that are input (initial state and results of reducer computations) or output (selector values) to state store are manipulated outside the store. To mitigate this, Slicey.Net deeply clones all the objects that are inputs or outputs to the store.
If this is too expensive for your use case, you override default cloning behavior to either
shallow cloning or no cloning by using the cloningLevel argument of the RootStateStore.
The same clonning level is used in all SliceStateStores based on this RootStateStore.
Dependency injection
Slicey.Net exposes RegisterRootStore<TStore, TStoreType> and RegisterSliceStore<TStore, TRootType, TStoreType>
that can be used to inject the classes in the dependency container. Stores are injected as singletons.
Following the example for slice stores, these would be injected as follows (Slicey.Net supports injecting into
IHostBuilder, IHostApplicationBuilder and directly into IServiceCollection):
var builder = WebApplication.CreateBuilder(args);
var initialState = new AppStateStore();
builder.RegisterRootStore<AppStateStore, AppState>(initialState);
builder.RegisterSliceStore<ModuleAStore, AppState, ModuleAState>();
builder.RegisterSliceStore<ModuleBStore, AppState, ModuleBState>();
Examples
There is an example .NET Blazor app that demonstrates how to use sliced store in practice
Information / bug reports / inquiries
For bug reports, inquiries, and any other information use <username> at gmail.com where my username is petar.vukmirovic2
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- DeepCloner (>= 0.10.4)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 201 | 7/27/2024 |