diff --git a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs
index 4d2cfae47f..88442dcac2 100644
--- a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs
+++ b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs
@@ -6,7 +6,6 @@
// Manages all user session data
//-----------------------------------------------------------------------
-using System.Collections.Concurrent;
using Csla.State;
namespace Csla.Blazor.State
@@ -16,23 +15,30 @@ namespace Csla.Blazor.State
/// root DI container.
///
///
- public class SessionManager(ISessionIdManager sessionIdManager) : ISessionManager
+ ///
+ public class SessionManager(ISessionIdManager sessionIdManager, ISessionStore sessionStore) : ISessionManager
{
- private readonly ConcurrentDictionary _sessions = [];
private readonly ISessionIdManager _sessionIdManager = sessionIdManager;
+ private readonly ISessionStore _sessionStore = sessionStore;
///
/// Gets the session data for the current user.
///
public Session GetSession()
{
- Session result;
var key = _sessionIdManager.GetSessionId();
- if (!_sessions.ContainsKey(key))
- _sessions.TryAdd(key, []);
- result = _sessions[key];
- result.Touch();
- return result;
+ var session = _sessionStore.GetSession(key);
+ if (session == null)
+ {
+ session = [];
+ session.Touch();
+ _sessionStore.CreateSession(key, session);
+ return session;
+ }
+
+ session.Touch();
+ _sessionStore.UpdateSession(key, session);
+ return session;
}
///
@@ -44,9 +50,19 @@ public void UpdateSession(Session newSession)
{
ArgumentNullException.ThrowIfNull(newSession);
var key = _sessionIdManager.GetSessionId();
- var existingSession = _sessions[key];
+ var existingSession = _sessionStore.GetSession(key)!;
Replace(newSession, existingSession);
existingSession.Touch();
+ _sessionStore.UpdateSession(key, existingSession);
+ }
+
+ ///
+ /// Remove all expired session data.
+ ///
+ /// Expiration duration
+ public void PurgeSessions(TimeSpan expiration)
+ {
+ _sessionStore.DeleteSessions(new SessionsFilter { Expiration = expiration });
}
///
@@ -62,21 +78,6 @@ private static void Replace(Session newSession, Session oldSession)
oldSession.Add(key, newSession[key]);
}
- ///
- /// Remove all expired session data.
- ///
- /// Expiration duration
- public void PurgeSessions(TimeSpan expiration)
- {
- var expirationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - expiration.TotalSeconds;
- List toRemove = [];
- foreach (var session in _sessions)
- if (session.Value.LastTouched < expirationTime)
- toRemove.Add(session.Key);
- foreach (var key in toRemove)
- _sessions.TryRemove(key, out _);
- }
-
// wasm client-side methods
Task ISessionManager.RetrieveSession(TimeSpan timeout) => throw new NotImplementedException();
Session ISessionManager.GetCachedSession() => throw new NotImplementedException();
diff --git a/Source/Csla.Blazor/ConfigurationExtensions.cs b/Source/Csla.Blazor/ConfigurationExtensions.cs
index cbbb288c07..e4b630d22b 100644
--- a/Source/Csla.Blazor/ConfigurationExtensions.cs
+++ b/Source/Csla.Blazor/ConfigurationExtensions.cs
@@ -6,6 +6,7 @@
// Implement extension methods for .NET Core configuration
//-----------------------------------------------------------------------
+using System.Diagnostics.CodeAnalysis;
using Csla.Blazor;
using Csla.Blazor.State;
using Csla.Core;
@@ -63,6 +64,7 @@ public static CslaOptions AddServerSideBlazor(this CslaOptions config, Action();
}
@@ -105,5 +107,27 @@ public class BlazorServerConfigurationOptions
/// Gets or sets the type of the ISessionIdManager service.
///
public Type SessionIdManagerType { get; set; } = Type.GetType("Csla.Blazor.State.SessionIdManager, Csla.AspNetCore", true);
+
+ ///
+ /// Gets or sets the type of the SessionStore.
+ ///
+#if NET8_0_OR_GREATER
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ internal Type SessionStoreType { get; set; } = typeof(DefaultSessionStore);
+
+ ///
+ /// Sets the type of the SessionStore.
+ ///
+ ///
+ public BlazorServerConfigurationOptions RegisterSessionStore<
+#if NET8_0_OR_GREATER
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+#endif
+ T>() where T: ISessionStore
+ {
+ SessionStoreType = typeof(T);
+ return this;
+ }
}
}
diff --git a/Source/Csla.Blazor/State/DefaultSessionStore.cs b/Source/Csla.Blazor/State/DefaultSessionStore.cs
new file mode 100644
index 0000000000..7281328c7b
--- /dev/null
+++ b/Source/Csla.Blazor/State/DefaultSessionStore.cs
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) Marimer LLC. All rights reserved.
+// Website: https://cslanet.com
+//
+// Default implementation of session storage.
+//-----------------------------------------------------------------------
+#nullable enable
+
+using System.Collections.Concurrent;
+using Csla.State;
+
+namespace Csla.Blazor.State
+{
+ ///
+ /// Default implementation of
+ ///
+ public class DefaultSessionStore : ISessionStore
+ {
+ private readonly ConcurrentDictionary _store = new();
+
+ ///
+ public Session? GetSession(string key)
+ {
+ _store.TryGetValue(key, out var item);
+ return item;
+ }
+
+ ///
+ public void CreateSession(string key, Session session)
+ {
+ if (!_store.TryAdd(key, session))
+ {
+ throw new Exception("Key already exists");
+ }
+ }
+
+ ///
+ public void UpdateSession(string key, Session session)
+ {
+ ArgumentNullException.ThrowIfNull(session);
+ _store[key] = session;
+ }
+
+ ///
+ public void DeleteSession(string key)
+ {
+ _store.TryRemove(key, out _);
+ }
+
+ ///
+ public void DeleteSessions(SessionsFilter filter)
+ {
+ filter.Validate();
+
+ var query = _store.AsQueryable();
+ if (filter.Expiration.HasValue)
+ {
+ var expirationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - filter.Expiration.Value.TotalSeconds;
+ query = query.Where(x => x.Value.LastTouched < expirationTime);
+ }
+
+ var keys = query.Select(x => x.Key).ToArray();
+
+ foreach (var key in keys)
+ {
+ _store.TryRemove(key, out _);
+ }
+ }
+ }
+}
diff --git a/Source/Csla/State/ISessionStore.cs b/Source/Csla/State/ISessionStore.cs
new file mode 100644
index 0000000000..6367839ada
--- /dev/null
+++ b/Source/Csla/State/ISessionStore.cs
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) Marimer LLC. All rights reserved.
+// Website: https://cslanet.com
+//
+// Manages session storage
+//-----------------------------------------------------------------------
+#nullable enable
+
+namespace Csla.State
+{
+ ///
+ /// Session store
+ ///
+ public interface ISessionStore
+ {
+ ///
+ /// Retrieves a session
+ ///
+ ///
+ /// The session for the given key, or default if not found
+ Session? GetSession(string key);
+
+ ///
+ /// Creates a session
+ ///
+ ///
+ ///
+ void CreateSession(string key, Session session);
+
+ ///
+ /// Updates a session
+ ///
+ ///
+ ///
+ void UpdateSession(string key, Session session);
+
+ ///
+ /// Deletes a session
+ ///
+ ///
+ void DeleteSession(string key);
+
+ ///
+ /// Deletes sessions based on the filter.
+ ///
+ ///
+ void DeleteSessions(SessionsFilter filter);
+ }
+}
diff --git a/Source/Csla/State/SessionsFilter.cs b/Source/Csla/State/SessionsFilter.cs
new file mode 100644
index 0000000000..ff2094ea10
--- /dev/null
+++ b/Source/Csla/State/SessionsFilter.cs
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) Marimer LLC. All rights reserved.
+// Website: https://cslanet.com
+//
+// Filter for querying session storage
+//-----------------------------------------------------------------------
+#nullable enable
+
+namespace Csla.State
+{
+ ///
+ /// Filter to query sessions
+ ///
+ public class SessionsFilter
+ {
+ ///
+ /// A timespan to filter sessions last touched after the expiration
+ ///
+ public TimeSpan? Expiration { get; set; }
+
+ ///
+ /// Validates
+ ///
+ public void Validate()
+ {
+ if (!Expiration.HasValue)
+ {
+ throw new ArgumentNullException("Expiration is required.");
+ }
+ }
+ }
+}