diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index 7d944888c843..6f79305687c0 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -29,6 +29,7 @@ and are generated based on the last package release.
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 60a03e72adfd..6a76ee051d97 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -51,6 +51,10 @@
https://github.com/dotnet/dotnet
20fdc50b34ee89e7c54eef0a193c30ed4816597a
+
+ https://github.com/dotnet/dotnet
+ f485d74550db5bd91617accc1bb548ae6013756b
+
https://github.com/dotnet/dotnet
20fdc50b34ee89e7c54eef0a193c30ed4816597a
diff --git a/eng/Versions.props b/eng/Versions.props
index 31308b813cfe..7ae3fd4c16fb 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -74,6 +74,7 @@
10.0.0-preview.6.25311.102
10.0.0-preview.6.25311.102
10.0.0-preview.6.25311.102
+ 10.0.0-alpha.2.24462.2
10.0.0-preview.6.25311.102
10.0.0-preview.6.25311.102
10.0.0-preview.6.25311.102
diff --git a/src/Components/Server/src/CircuitOptions.cs b/src/Components/Server/src/CircuitOptions.cs
index 7c63f0ef361e..d9904e5a3699 100644
--- a/src/Components/Server/src/CircuitOptions.cs
+++ b/src/Components/Server/src/CircuitOptions.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.Extensions.Caching.Hybrid;
+
namespace Microsoft.AspNetCore.Components.Server;
///
@@ -49,17 +51,40 @@ public sealed class CircuitOptions
/// are retained in memory by the server when no distributed cache is configured.
///
///
- /// When using a distributed cache like this value is ignored
+ ///
+ /// When using a distributed cache like this value is ignored
/// and the configuration from
/// is used instead.
+ ///
+ ///
+ /// To explicitly control the in memory cache limits when using a distributed cache. Setup a separate instance in with
+ /// the desired configuration.
+ ///
///
public int PersistedCircuitInMemoryMaxRetained { get; set; } = 1000;
///
/// Gets or sets the duration for which a persisted circuit is retained in memory.
///
+ ///
+ /// When using a based implementation this value
+ /// is used for the local cache retention period.
+ ///
public TimeSpan PersistedCircuitInMemoryRetentionPeriod { get; set; } = TimeSpan.FromHours(2);
+ ///
+ /// Gets or sets the duration for which a persisted circuit is retained in the distributed cache.
+ ///
+ ///
+ /// This setting is ignored when using an in-memory cache implementation.
+ ///
+ public TimeSpan? PersistedCircuitDistributedRetentionPeriod { get; set; } = TimeSpan.FromHours(8);
+
+ ///
+ /// Gets or sets the instance to use for persisting circuit state across servers.
+ ///
+ public HybridCache? HybridPersistenceCache { get; set; }
+
///
/// Gets or sets a value that determines whether or not to send detailed exception messages to JavaScript when an unhandled exception
/// happens on the circuit or when a .NET method invocation through JS interop results in an exception.
diff --git a/src/Components/Server/src/Circuits/DefaultHybridCache.cs b/src/Components/Server/src/Circuits/DefaultHybridCache.cs
new file mode 100644
index 000000000000..6531a388ae23
--- /dev/null
+++ b/src/Components/Server/src/Circuits/DefaultHybridCache.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Components.Server.Circuits;
+
+///
+/// Default configuration for .
+///
+internal sealed class DefaultHybridCache : IPostConfigureOptions
+{
+ private readonly HybridCache? _hybridCache;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The service, if available.
+ public DefaultHybridCache(HybridCache? hybridCache = null)
+ {
+ _hybridCache = hybridCache;
+ }
+
+ ///
+ public void PostConfigure(string? name, CircuitOptions options)
+ {
+ // Only set the HybridPersistenceCache if it hasn't been explicitly configured
+ // and a HybridCache service is available
+ if (options.HybridPersistenceCache is null && _hybridCache is not null)
+ {
+ options.HybridPersistenceCache = _hybridCache;
+ }
+ }
+}
diff --git a/src/Components/Server/src/Circuits/HybridCacheCircuitPersistenceProvider.cs b/src/Components/Server/src/Circuits/HybridCacheCircuitPersistenceProvider.cs
new file mode 100644
index 000000000000..d97b392aa937
--- /dev/null
+++ b/src/Components/Server/src/Circuits/HybridCacheCircuitPersistenceProvider.cs
@@ -0,0 +1,131 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Components.Server.Circuits;
+
+// Implementation of ICircuitPersistenceProvider that uses HybridCache for distributed caching
+internal sealed partial class HybridCacheCircuitPersistenceProvider : ICircuitPersistenceProvider
+{
+ private static readonly Func> _failOnCreate =
+ static ct => throw new InvalidOperationException();
+
+ private static readonly string[] _tags = ["Microsoft.AspNetCore.Components.Server.PersistedCircuitState"];
+
+ private readonly SemaphoreSlim _lock = new(1, 1);
+ private readonly HybridCache _hybridCache;
+ private readonly ILogger _logger;
+ private readonly HybridCacheEntryOptions _cacheWriteOptions;
+ private readonly HybridCacheEntryOptions _cacheReadOptions;
+
+ public HybridCacheCircuitPersistenceProvider(
+ HybridCache hybridCache,
+ ILogger logger,
+ IOptions options)
+ {
+ _hybridCache = hybridCache;
+ _logger = logger;
+ _cacheWriteOptions = new HybridCacheEntryOptions
+ {
+ Expiration = options.Value.PersistedCircuitDistributedRetentionPeriod,
+ LocalCacheExpiration = options.Value.PersistedCircuitInMemoryRetentionPeriod,
+ };
+ _cacheReadOptions = new HybridCacheEntryOptions
+ {
+ Flags = HybridCacheEntryFlags.DisableLocalCacheWrite |
+ HybridCacheEntryFlags.DisableDistributedCacheWrite |
+ HybridCacheEntryFlags.DisableUnderlyingData,
+ };
+ }
+
+ public async Task PersistCircuitAsync(CircuitId circuitId, PersistedCircuitState persistedCircuitState, CancellationToken cancellation = default)
+ {
+ Log.CircuitPauseStarted(_logger, circuitId);
+
+ try
+ {
+ await _lock.WaitAsync(cancellation);
+ await _hybridCache.SetAsync(circuitId.Secret, persistedCircuitState, _cacheWriteOptions, _tags, cancellation);
+ }
+ catch (Exception ex)
+ {
+ Log.ExceptionPersistingCircuit(_logger, circuitId, ex);
+ }
+ finally
+ {
+ _lock.Release();
+ }
+ }
+
+ public async Task RestoreCircuitAsync(CircuitId circuitId, CancellationToken cancellation = default)
+ {
+ Log.CircuitResumeStarted(_logger, circuitId);
+
+ try
+ {
+ await _lock.WaitAsync(cancellation);
+ var state = await _hybridCache.GetOrCreateAsync(
+ circuitId.Secret,
+ factory: _failOnCreate,
+ options: _cacheReadOptions,
+ _tags,
+ cancellation);
+
+ if (state == null)
+ {
+ Log.FailedToFindCircuitState(_logger, circuitId);
+ return null;
+ }
+
+ await _hybridCache.RemoveAsync(circuitId.Secret, cancellation);
+
+ Log.CircuitStateFound(_logger, circuitId);
+ return state;
+ }
+ catch (Exception ex)
+ {
+ Log.ExceptionRestoringCircuit(_logger, circuitId, ex);
+ return null;
+ }
+ finally
+ {
+ _lock.Release();
+ }
+ }
+
+ private static partial class Log
+ {
+ [LoggerMessage(201, LogLevel.Debug, "Circuit state evicted for circuit {CircuitId} due to {Reason}", EventName = "CircuitStateEvicted")]
+ public static partial void CircuitStateEvicted(ILogger logger, CircuitId circuitId, string reason);
+
+ [LoggerMessage(202, LogLevel.Debug, "Resuming circuit with ID {CircuitId}", EventName = "CircuitResumeStarted")]
+ public static partial void CircuitResumeStarted(ILogger logger, CircuitId circuitId);
+
+ [LoggerMessage(203, LogLevel.Debug, "Failed to find persisted circuit with ID {CircuitId}", EventName = "FailedToFindCircuitState")]
+ public static partial void FailedToFindCircuitState(ILogger logger, CircuitId circuitId);
+
+ [LoggerMessage(204, LogLevel.Debug, "Circuit state found for circuit {CircuitId}", EventName = "CircuitStateFound")]
+ public static partial void CircuitStateFound(ILogger logger, CircuitId circuitId);
+
+ [LoggerMessage(205, LogLevel.Error, "An exception occurred while disposing the token source.", EventName = "ExceptionDisposingTokenSource")]
+ public static partial void ExceptionDisposingTokenSource(ILogger logger, Exception exception);
+
+ [LoggerMessage(206, LogLevel.Debug, "Pausing circuit with ID {CircuitId}", EventName = "CircuitPauseStarted")]
+ public static partial void CircuitPauseStarted(ILogger logger, CircuitId circuitId);
+
+ [LoggerMessage(207, LogLevel.Error, "An exception occurred while persisting circuit {CircuitId}.", EventName = "ExceptionPersistingCircuit")]
+ public static partial void ExceptionPersistingCircuit(ILogger logger, CircuitId circuitId, Exception exception);
+
+ [LoggerMessage(208, LogLevel.Error, "An exception occurred while restoring circuit {CircuitId}.", EventName = "ExceptionRestoringCircuit")]
+ public static partial void ExceptionRestoringCircuit(ILogger logger, CircuitId circuitId, Exception exception);
+
+ [LoggerMessage(209, LogLevel.Error, "An exception occurred during expiration handling for circuit {CircuitId}.", EventName = "ExceptionDuringExpiration")]
+ public static partial void ExceptionDuringExpiration(ILogger logger, CircuitId circuitId, Exception exception);
+
+ [LoggerMessage(210, LogLevel.Error, "An exception occurred while removing expired circuit {CircuitId}.", EventName = "ExceptionRemovingExpiredCircuit")]
+ public static partial void ExceptionRemovingExpiredCircuit(ILogger logger, CircuitId circuitId, Exception exception);
+ }
+}
diff --git a/src/Components/Server/src/Circuits/PersistedCircuitState.cs b/src/Components/Server/src/Circuits/PersistedCircuitState.cs
index 81f1277d66ad..e02536f925dc 100644
--- a/src/Components/Server/src/Circuits/PersistedCircuitState.cs
+++ b/src/Components/Server/src/Circuits/PersistedCircuitState.cs
@@ -8,9 +8,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits;
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
internal class PersistedCircuitState
{
- public IReadOnlyDictionary ApplicationState { get; internal set; }
+ public IReadOnlyDictionary ApplicationState { get; set; }
- public byte[] RootComponents { get; internal set; }
+ public byte[] RootComponents { get; set; }
private string GetDebuggerDisplay()
{
diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
index a93fc5a9da95..f63b514fe0f6 100644
--- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
+++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
@@ -15,6 +15,7 @@
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
@@ -77,11 +78,29 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
services.TryAddScoped(s => s.GetRequiredService().Circuit);
services.TryAddScoped();
-
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
- services.TryAddSingleton();
+
+ // Register the circuit persistence provider conditionally based on HybridCache availability
+ services.TryAddSingleton(serviceProvider =>
+ {
+ var circuitOptions = serviceProvider.GetRequiredService>();
+ if (circuitOptions.Value.HybridPersistenceCache is not null)
+ {
+ var logger = serviceProvider.GetRequiredService>();
+ return new HybridCacheCircuitPersistenceProvider(circuitOptions.Value.HybridPersistenceCache, logger, circuitOptions);
+ }
+ else
+ {
+ var logger = serviceProvider.GetRequiredService>();
+ var clock = serviceProvider.GetRequiredService();
+ return new DefaultInMemoryCircuitPersistenceProvider(clock, logger, circuitOptions);
+ }
+ });
+
+ // Register the configurator for HybridCache
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, DefaultHybridCache>());
// Standard blazor hosting services implementations
//
diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt
index a59d7ec1457c..210a9b4fdd3c 100644
--- a/src/Components/Server/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Server/src/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.AspNetCore.Components.Server.CircuitOptions.HybridPersistenceCache.get -> Microsoft.Extensions.Caching.Hybrid.HybridCache?
+Microsoft.AspNetCore.Components.Server.CircuitOptions.HybridPersistenceCache.set -> void
+Microsoft.AspNetCore.Components.Server.CircuitOptions.PersistedCircuitDistributedRetentionPeriod.get -> System.TimeSpan?
+Microsoft.AspNetCore.Components.Server.CircuitOptions.PersistedCircuitDistributedRetentionPeriod.set -> void
Microsoft.AspNetCore.Components.Server.CircuitOptions.PersistedCircuitInMemoryMaxRetained.get -> int
Microsoft.AspNetCore.Components.Server.CircuitOptions.PersistedCircuitInMemoryMaxRetained.set -> void
Microsoft.AspNetCore.Components.Server.CircuitOptions.PersistedCircuitInMemoryRetentionPeriod.get -> System.TimeSpan
diff --git a/src/Components/Server/test/Circuits/HybridCacheCircuitPersistenceProviderTest.cs b/src/Components/Server/test/Circuits/HybridCacheCircuitPersistenceProviderTest.cs
new file mode 100644
index 000000000000..ade8e56eb769
--- /dev/null
+++ b/src/Components/Server/test/Circuits/HybridCacheCircuitPersistenceProviderTest.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Server.Circuits;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits;
+
+public class HybridCacheCircuitPersistenceProviderTest
+{
+ [Fact]
+ public async Task CanPersistAndRestoreState()
+ {
+ // Arrange
+ var hybridCache = CreateHybridCache();
+ var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
+ var persistedState = new PersistedCircuitState()
+ {
+ RootComponents = [1, 2, 3],
+ ApplicationState = new Dictionary {
+ { "key1", new byte[] { 4, 5, 6 } },
+ { "key2", new byte[] { 7, 8, 9 } }
+ }
+ };
+ var provider = CreateProvider(hybridCache);
+
+ // Act
+ await provider.PersistCircuitAsync(circuitId, persistedState);
+
+ // Assert
+ var result = await provider.RestoreCircuitAsync(circuitId);
+ Assert.NotNull(result);
+ Assert.Equal(persistedState.RootComponents, result.RootComponents);
+ Assert.Equal(persistedState.ApplicationState, result.ApplicationState);
+ }
+
+ [Fact]
+ public async Task RestoreCircuitAsync_ReturnsNull_WhenCircuitDoesNotExist()
+ {
+ // Arrange
+ var hybridCache = CreateHybridCache();
+ var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
+ var provider = CreateProvider(hybridCache);
+ var cacheKey = circuitId.Secret;
+
+ // Act
+ var result = await provider.RestoreCircuitAsync(circuitId);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task RestoreCircuitAsync_RemovesCircuitFromCache()
+ {
+ // Arrange
+ var hybridCache = CreateHybridCache();
+ var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
+ var persistedState = new PersistedCircuitState()
+ {
+ RootComponents = [1, 2, 3],
+ ApplicationState = new Dictionary {
+ { "key1", new byte[] { 4, 5, 6 } },
+ { "key2", new byte[] { 7, 8, 9 } }
+ }
+ };
+
+ var provider = CreateProvider(hybridCache);
+ var cacheKey = circuitId.Secret;
+
+ await provider.PersistCircuitAsync(circuitId, persistedState);
+
+ // Act
+ var result1 = await provider.RestoreCircuitAsync(circuitId);
+ var result2 = await provider.RestoreCircuitAsync(circuitId);
+
+ // Assert
+ Assert.NotNull(result1);
+ Assert.Equal(persistedState.RootComponents, result1.RootComponents);
+ Assert.Equal(persistedState.ApplicationState, result1.ApplicationState);
+
+ Assert.Null(result2); // Circuit should be removed after first restore
+ }
+
+ private HybridCache CreateHybridCache()
+ {
+ return new ServiceCollection()
+ .AddHybridCache().Services
+ .BuildServiceProvider()
+ .GetRequiredService();
+ }
+
+ private static HybridCacheCircuitPersistenceProvider CreateProvider(
+ HybridCache hybridCache,
+ CircuitOptions options = null)
+ {
+ return new HybridCacheCircuitPersistenceProvider(
+ hybridCache,
+ NullLogger.Instance,
+ Options.Create(options ?? new CircuitOptions()));
+ }
+}
diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj
index 341fa72218a0..d0ca069cc1c3 100644
--- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj
+++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerResumeTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerResumeTests.cs
index 5ad43fc2c634..f79d5064c8b4 100644
--- a/src/Components/test/E2ETest/ServerExecutionTests/ServerResumeTests.cs
+++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerResumeTests.cs
@@ -208,3 +208,15 @@ protected override void InitializeAsyncCore()
Browser.Exists(By.CssSelector("#components-reconnect-modal[data-nosnippet]"));
}
}
+
+public class HybridCacheServerResumeTests : ServerResumeTests
+{
+ public HybridCacheServerResumeTests(
+ BrowserFixture browserFixture,
+ BasicTestAppServerSiteFixture> serverFixture,
+ ITestOutputHelper output)
+ : base(browserFixture, serverFixture, output)
+ {
+ serverFixture.AdditionalArguments.AddRange("--UseHybridCache", "true");
+ }
+}
diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
index 1341319c942c..b7d6d2b019b8 100644
--- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
+++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
@@ -28,6 +28,7 @@
+
diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs
index bbe5562ddf3a..800ad27ddce4 100644
--- a/src/Components/test/testassets/Components.TestServer/Program.cs
+++ b/src/Components/test/testassets/Components.TestServer/Program.cs
@@ -33,6 +33,7 @@ public static async Task Main(string[] args)
["Server-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Blazor web with server-side blazor root component"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"),
["Blazor web with server-side reconnection disabled"] = (BuildWebHost>(CreateAdditionalArgs([.. args, "--DisableReconnectionCache", "true"])), "/subdir"),
+ ["Blazor web with server-side hybrid cache"] = (BuildWebHost>(CreateAdditionalArgs([.. args, "--DisableReconnectionCache", "true", "--UseHybridCache", "true"])), "/subdir"),
["Hosted client-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Hot Reload"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Dev server client-side blazor"] = CreateDevServerHost(CreateAdditionalArgs(args)),
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
index eedeacc7ef57..4bb4bee5014c 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
@@ -44,6 +44,7 @@ public void ConfigureServices(IServiceCollection services)
{
// This disables the reconnection cache, which forces the server to persist the circuit state.
options.DisconnectedCircuitMaxRetained = 0;
+ options.DetailedErrors = true;
}
})
.AddAuthenticationStateSerialization(options =>
@@ -52,6 +53,11 @@ public void ConfigureServices(IServiceCollection services)
options.SerializeAllClaims = serializeAllClaims;
});
+ if (Configuration.GetValue("UseHybridCache"))
+ {
+ services.AddHybridCache();
+ }
+
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor
index e6c80d54a12a..07a5daa32bde 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor
@@ -65,7 +65,7 @@
startButton.textContent = 'Call Blazor.start()';
startButton.onclick = callBlazorStart;
document.body.appendChild(startButton);
- }
+ }
@if(CustomReconnectUI) {