diff --git a/Source/Lib/Fluxor.Blazor.Web/Components/FluxorComponent.cs b/Source/Lib/Fluxor.Blazor.Web/Components/FluxorComponent.cs
index 35c7378e..24544b61 100644
--- a/Source/Lib/Fluxor.Blazor.Web/Components/FluxorComponent.cs
+++ b/Source/Lib/Fluxor.Blazor.Web/Components/FluxorComponent.cs
@@ -1,6 +1,8 @@
using Fluxor.UnsupportedClasses;
using Microsoft.AspNetCore.Components;
+using System.Threading;
using System;
+using System.Threading.Tasks;
namespace Fluxor.Blazor.Web.Components
{
@@ -13,6 +15,9 @@ public abstract class FluxorComponent : ComponentBase, IDisposable
[Inject]
private IActionSubscriber ActionSubscriber { get; set; }
+ [Inject]
+ private IStore Store { get; set; }
+
private bool Disposed;
private IDisposable StateSubscription;
private readonly ThrottledInvoker StateHasChangedThrottler;
@@ -20,7 +25,7 @@ public abstract class FluxorComponent : ComponentBase, IDisposable
///
/// Creates a new instance
///
- public FluxorComponent()
+ protected FluxorComponent()
{
StateHasChangedThrottler = new ThrottledInvoker(() =>
{
@@ -63,17 +68,25 @@ public void Dispose()
Disposed = true;
}
}
-
+
///
/// Subscribes to state properties
///
protected override void OnInitialized()
{
- base.OnInitialized();
StateSubscription = StateSubscriber.Subscribe(this, _ =>
{
StateHasChangedThrottler.Invoke(MaximumStateChangedNotificationsPerSecond);
});
+
+ base.OnInitialized();
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ //Attempt to initialize the store knowing that if it's already been initialized, this won't do anything.
+ await Store.InitializeAsync();
+ await base.OnInitializedAsync();
}
protected virtual void Dispose(bool disposing)
diff --git a/Source/Lib/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj b/Source/Lib/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj
index ee56a2b5..6c8c529a 100644
--- a/Source/Lib/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj
+++ b/Source/Lib/Fluxor.Blazor.Web/Fluxor.Blazor.Web.csproj
@@ -12,9 +12,11 @@
+
+
diff --git a/Source/Lib/Fluxor.Blazor.Web/Persistence/PersistenceEffects.cs b/Source/Lib/Fluxor.Blazor.Web/Persistence/PersistenceEffects.cs
new file mode 100644
index 00000000..56a600b3
--- /dev/null
+++ b/Source/Lib/Fluxor.Blazor.Web/Persistence/PersistenceEffects.cs
@@ -0,0 +1,59 @@
+using Fluxor.Persistence;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Fluxor.Blazor.Web.Persistence
+{
+#if NET6_0_OR_GREATER
+ public sealed class PersistenceEffects
+ {
+ private readonly IPersistenceManager _persistenceManager;
+ private readonly IServiceProvider _serviceProvider;
+
+ public PersistenceEffects(IPersistenceManager persistenceManager, IServiceProvider serviceProvider)
+ {
+ _persistenceManager = persistenceManager;
+ _serviceProvider = serviceProvider;
+ }
+
+ ///
+ /// Maintains a reference to IStore - injected this way to avoid a circular dependency during the effect method registration
+ ///
+ private readonly Lazy _store = new(_serviceProvider.GetRequiredService);
+
+ [EffectMethod(typeof(StorePersistingAction))]
+ public async Task PersistStoreData(IDispatcher dispatcher)
+ {
+ //Serialize the store
+ var json = _store.Value.SerializeToJson();
+
+ //Save to the persistence manager
+ await _persistenceManager.PersistStoreToStateAsync(json);
+
+ //Completed
+ dispatcher.Dispatch(new StorePersistedAction());
+ }
+
+ [EffectMethod(typeof(StoreRehydratingAction))]
+ public async Task RehydrateStoreData(IDispatcher dispatcher)
+ {
+ //Read from the persistence manager
+ var serializedStore = await _persistenceManager.RehydrateStoreFromStateAsync();
+ if (serializedStore is null)
+ {
+ //Nothing to rehydrate - leave as-is
+ dispatcher.Dispatch(new StoreRehydratedAction());
+ return;
+ }
+
+ _store.Value.RehydrateFromJson(serializedStore);
+
+ //Completed
+ dispatcher.Dispatch(new StoreRehydratedAction());
+ }
+ }
+#endif
+}
diff --git a/Source/Lib/Fluxor.Blazor.Web/StoreInitializer.cs b/Source/Lib/Fluxor.Blazor.Web/StoreInitializer.cs
index fa244d78..01bc364f 100644
--- a/Source/Lib/Fluxor.Blazor.Web/StoreInitializer.cs
+++ b/Source/Lib/Fluxor.Blazor.Web/StoreInitializer.cs
@@ -49,6 +49,14 @@ protected override void OnInitialized()
base.OnInitialized();
}
+#if NET8
+ protected override async Task OnInitializedAsync()
+ {
+ await Store.InitializeAsync();
+ await base.OnInitializedAsync();
+ }
+#endif
+
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
@@ -73,7 +81,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (!string.IsNullOrWhiteSpace(MiddlewareInitializationScripts))
await JSRuntime.InvokeVoidAsync("eval", MiddlewareInitializationScripts);
+#if !NET8
await Store.InitializeAsync();
+#endif
}
catch (JSException err)
{
diff --git a/Source/Lib/Fluxor/DependencyInjection/FluxorOptions.cs b/Source/Lib/Fluxor/DependencyInjection/FluxorOptions.cs
index d0c0e848..a6c8427d 100644
--- a/Source/Lib/Fluxor/DependencyInjection/FluxorOptions.cs
+++ b/Source/Lib/Fluxor/DependencyInjection/FluxorOptions.cs
@@ -1,4 +1,5 @@
using Fluxor.Extensions;
+using Fluxor.Persistence;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
@@ -138,5 +139,32 @@ public FluxorOptions AddMiddleware()
.ToArray();
return this;
}
+
+#if NET6_0_OR_GREATER
+
+ ///
+ /// Enables automatic store persistence between location state changes.
+ ///
+ /// The type of persistence implementation to use.
+ /// Options
+ public FluxorOptions WithPersistence() where T : IPersistenceManager
+ {
+ Services.AddScoped(typeof(IPersistenceManager), typeof(T));
+ return this;
+ }
+
+ ///
+ /// Enables automatic store persistence between location state changes.
+ ///
+ /// The type of persistence implementation to use.
+ /// Options
+ public FluxorOptions WithPersistence(IServiceCollection serviceCollection) where T : IPersistenceManager
+ {
+ WithPersistence();
+ foreach (var serviceDescriptor in serviceCollection) Services.Add(serviceDescriptor);
+ return this;
+ }
+
+#endif
}
}
diff --git a/Source/Lib/Fluxor/DependencyInjection/ServiceRegistration/StoreRegistration.cs b/Source/Lib/Fluxor/DependencyInjection/ServiceRegistration/StoreRegistration.cs
index 50929e2d..9d43d6d3 100644
--- a/Source/Lib/Fluxor/DependencyInjection/ServiceRegistration/StoreRegistration.cs
+++ b/Source/Lib/Fluxor/DependencyInjection/ServiceRegistration/StoreRegistration.cs
@@ -1,5 +1,6 @@
using Fluxor.DependencyInjection.WrapperFactories;
using Fluxor.Extensions;
+using Fluxor.Persistence;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
@@ -41,7 +42,13 @@ public static void Register(
services.Add(typeof(Store), serviceProvider =>
{
var dispatcher = serviceProvider.GetService();
+
+#if NET6_0_OR_GREATER
+ var persistenceManager = serviceProvider.GetService();
+ var store = new Store(dispatcher, persistenceManager);
+#else
var store = new Store(dispatcher);
+#endif
foreach (FeatureClassInfo featureClassInfo in featureClassInfos)
{
var feature = (IFeature)serviceProvider.GetService(featureClassInfo.FeatureInterfaceGenericType);
diff --git a/Source/Lib/Fluxor/Fluxor.csproj b/Source/Lib/Fluxor/Fluxor.csproj
index f607192c..03ffba61 100644
--- a/Source/Lib/Fluxor/Fluxor.csproj
+++ b/Source/Lib/Fluxor/Fluxor.csproj
@@ -4,7 +4,7 @@
A zero boilerplate Redux/Flux framework for .NET
fluxor-small-logo.png
Redux Flux DotNet CSharp
- True
+ True
@@ -15,9 +15,11 @@
+
+
diff --git a/Source/Lib/Fluxor/IStore.cs b/Source/Lib/Fluxor/IStore.cs
index 03b1cde5..03913a21 100644
--- a/Source/Lib/Fluxor/IStore.cs
+++ b/Source/Lib/Fluxor/IStore.cs
@@ -93,5 +93,18 @@ public interface IStore : IActionSubscriber
/// Executed when an exception is not handled
///
event EventHandler UnhandledException;
+
+ ///
+ /// Persists the features in the store to a serialized string.
+ ///
+ ///
+ string SerializeToJson();
+
+ ///
+ /// Deserializes a previously serialized store and rehydrates each feature using the
+ /// provided values.
+ ///
+ /// The serialized store.
+ void RehydrateFromJson(string json);
}
}
diff --git a/Source/Lib/Fluxor/Persistence/IPersistenceManager.cs b/Source/Lib/Fluxor/Persistence/IPersistenceManager.cs
new file mode 100644
index 00000000..ea7f0ddc
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/IPersistenceManager.cs
@@ -0,0 +1,27 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Fluxor.Persistence
+{
+ public interface IPersistenceManager
+ {
+ ///
+ /// Persists the store to a persisted state.
+ ///
+ /// The serialized store data being persisted.
+ public Task PersistStoreToStateAsync(string serializedStore);
+
+ ///
+ /// Rehydrates the store from a persisted state.
+ ///
+ public Task RehydrateStoreFromStateAsync();
+
+ ///
+ /// Clears the store from the persisted state.
+ ///
+ public Task ClearStoreFromStateAsync();
+ }
+}
diff --git a/Source/Lib/Fluxor/Persistence/PersistenceEffects.cs b/Source/Lib/Fluxor/Persistence/PersistenceEffects.cs
new file mode 100644
index 00000000..bfd24a92
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/PersistenceEffects.cs
@@ -0,0 +1,56 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Fluxor.Persistence
+{
+ public sealed class PersistenceEffects
+ {
+ private readonly IPersistenceManager _persistenceManager;
+ private readonly IServiceProvider _serviceProvider;
+
+ public PersistenceEffects(IPersistenceManager persistenceManager, IServiceProvider serviceProvider)
+ {
+ _persistenceManager = persistenceManager;
+ _serviceProvider = serviceProvider;
+ }
+
+ ///
+ /// Maintains a reference to IStore - injected this way to avoid a circular dependency during the effect method registration
+ ///
+ private readonly Lazy _store = new(_serviceProvider.GetRequiredService);
+
+ [EffectMethod(typeof(StorePersistingAction))]
+ public async Task PersistStoreData(IDispatcher dispatcher)
+ {
+ //Serialize the store
+ var json = _store.Value.SerializeToJson();
+
+ //Save to the persistence manager
+ await _persistenceManager.PersistStoreToStateAsync(json);
+
+ //Completed
+ dispatcher.Dispatch(new StorePersistedAction());
+ }
+
+ [EffectMethod(typeof(StoreRehydratingAction))]
+ public async Task RehydrateStoreData(IDispatcher dispatcher)
+ {
+ //Read from the persistence manager
+ var serializedStore = await _persistenceManager.RehydrateStoreFromStateAsync();
+ if (serializedStore is null)
+ {
+ //Nothing to rehydrate - leave as-is
+ dispatcher.Dispatch(new StoreRehydratedAction());
+ return;
+ }
+
+ _store.Value.RehydrateFromJson(serializedStore);
+
+ //Completed
+ dispatcher.Dispatch(new StoreRehydratedAction());
+ }
+ }
+}
diff --git a/Source/Lib/Fluxor/Persistence/StorePersistedAction.cs b/Source/Lib/Fluxor/Persistence/StorePersistedAction.cs
new file mode 100644
index 00000000..99288929
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/StorePersistedAction.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Fluxor.Persistence
+{
+ ///
+ /// Dispatched by the store once it has been successfully persisted to state.
+ ///
+ public sealed class StorePersistedAction
+ {
+ }
+}
diff --git a/Source/Lib/Fluxor/Persistence/StorePersistingAction.cs b/Source/Lib/Fluxor/Persistence/StorePersistingAction.cs
new file mode 100644
index 00000000..f37943c1
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/StorePersistingAction.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Fluxor.Persistence
+{
+ ///
+ /// Dispatched by the store once it has started persisting to state.
+ ///
+ public sealed class StorePersistingAction
+ {
+ }
+}
diff --git a/Source/Lib/Fluxor/Persistence/StoreRehydratedAction.cs b/Source/Lib/Fluxor/Persistence/StoreRehydratedAction.cs
new file mode 100644
index 00000000..478c9098
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/StoreRehydratedAction.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Fluxor.Persistence
+{
+ ///
+ /// Dispatched by the store once it has been successfully rehydrated from state.
+ ///
+ public sealed class StoreRehydratedAction
+ {
+ }
+}
diff --git a/Source/Lib/Fluxor/Persistence/StoreRehydratingAction.cs b/Source/Lib/Fluxor/Persistence/StoreRehydratingAction.cs
new file mode 100644
index 00000000..62d0404f
--- /dev/null
+++ b/Source/Lib/Fluxor/Persistence/StoreRehydratingAction.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Fluxor.Persistence
+{
+ ///
+ /// Dispatched by the store once it has started rehydrating from state.
+ ///
+ public sealed class StoreRehydratingAction
+ {
+ }
+}
diff --git a/Source/Lib/Fluxor/Store.cs b/Source/Lib/Fluxor/Store.cs
index 56943278..476969a7 100644
--- a/Source/Lib/Fluxor/Store.cs
+++ b/Source/Lib/Fluxor/Store.cs
@@ -1,7 +1,14 @@
-using System;
+#nullable enable
+using Fluxor.Persistence;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+#if NET6_0_OR_GREATER
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Fluxor.Persistence;
+#endif
using System.Threading.Tasks;
namespace Fluxor
@@ -14,6 +21,10 @@ public class Store : IStore, IActionSubscriber, IDisposable
///
public Task Initialized => InitializedCompletionSource.Task;
+#if NET6_0_OR_GREATER
+ private readonly IPersistenceManager? _persistenceManager;
+#endif
+
private object SyncRoot = new object();
private bool Disposed;
private readonly IDispatcher Dispatcher;
@@ -30,16 +41,35 @@ public class Store : IStore, IActionSubscriber, IDisposable
private volatile bool HasActivatedStore;
private bool IsInsideMiddlewareChange => BeginMiddlewareChangeCount > 0;
+#if NET6_0_OR_GREATER
+
+ ///
+ /// Creates an instance of the store
+ ///
+ public Store(IDispatcher dispatcher, IPersistenceManager persistenceManager = null)
+ {
+
+ _persistenceManager = persistenceManager;
+ ActionSubscriber = new ActionSubscriber();
+ Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+ Dispatcher.ActionDispatched += ActionDispatched;
+ Dispatcher.Dispatch(new StoreInitializedAction());
+ }
+
+#else
+
///
/// Creates an instance of the store
///
public Store(IDispatcher dispatcher)
{
+
ActionSubscriber = new ActionSubscriber();
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Dispatcher.ActionDispatched += ActionDispatched;
Dispatcher.Dispatch(new StoreInitializedAction());
}
+#endif
///
public IEnumerable GetMiddlewares() => Middlewares;
@@ -265,8 +295,16 @@ private async Task ActivateStoreAsync()
lock (SyncRoot)
{
+#if NET6_0_OR_GREATER
+ if (_persistenceManager is not null)
+ {
+ //Rehydrate as necessary
+ Dispatcher.Dispatch(new StoreRehydratingAction());
+ }
+#endif
HasActivatedStore = true;
DequeueActions();
+
InitializedCompletionSource.SetResult(true);
}
}
@@ -279,6 +317,13 @@ private void DequeueActions()
IsDispatching = true;
try
{
+ //Only persist the store state if the action(s) in the queue don't consist solely of any combination of the following
+ if (!QueuedActions.IsEmpty && !QueuedActions.All(action => action is StoreInitializedAction or StoreRehydratingAction or StoreRehydratedAction or StorePersistingAction or StorePersistedAction))
+ {
+ //Add an action to the end of the queue that persists the results of the actions to state, skipping the dispatcher approach (which might lead to an infinite loop)
+ QueuedActions.Enqueue(new StorePersistingAction());
+ }
+
while (QueuedActions.TryDequeue(out object nextActionToProcess))
{
// Only process the action if no middleware vetos it
@@ -301,5 +346,42 @@ private void DequeueActions()
IsDispatching = false;
}
}
+
+#if NET6_0_OR_GREATER
+
+ public string SerializeToJson()
+ {
+ var rootObj = new JsonObject();
+ foreach (var kv in FeaturesByName)
+ {
+ var featureName = kv.Value.GetName();
+ var featureValue = kv.Value.GetState();
+ rootObj[featureName] = JsonSerializer.SerializeToNode(featureValue);
+ }
+
+ return JsonSerializer.Serialize(rootObj);
+ }
+
+ public void RehydrateFromJson(string json)
+ {
+ var obj = JsonDocument.Parse(json);
+
+ foreach (var feature in obj.RootElement.EnumerateObject())
+ {
+ //Replace the state in the named feature with what's in the serialized data
+ if (Features.ContainsKey(feature.Name))
+ {
+ var stateType = Features[feature.Name].GetStateType();
+ var featureValue = feature.Value.Deserialize(stateType);
+
+ if (featureValue is null)
+ continue;
+
+ FeaturesByName[feature.Name].RestoreState(featureValue);
+ }
+ }
+ }
+
+#endif
}
}