Skip to content

Commit aea8fca

Browse files
committed
Fixes
1 parent fae272b commit aea8fca

File tree

6 files changed

+72
-224
lines changed

6 files changed

+72
-224
lines changed

src/Components/Server/src/Circuits/HybridCacheCircuitPersistenceProvider.cs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -130,26 +130,3 @@ private static partial class Log
130130
public static partial void ExceptionRemovingExpiredCircuit(ILogger logger, CircuitId circuitId, Exception exception);
131131
}
132132
}
133-
134-
internal static class WaitHandleExtensions
135-
{
136-
public static async Task WaitOneAsync(this WaitHandle waitHandle, TimeSpan timeout)
137-
{
138-
var tcs = new TaskCompletionSource<bool>();
139-
var registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
140-
waitHandle,
141-
(state, timedOut) => tcs.SetResult(!timedOut),
142-
null,
143-
timeout,
144-
true);
145-
146-
try
147-
{
148-
await tcs.Task;
149-
}
150-
finally
151-
{
152-
registeredWaitHandle.Unregister(null);
153-
}
154-
}
155-
}

src/Components/Server/src/Circuits/PersistedCircuitState.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits;
88
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
99
internal class PersistedCircuitState
1010
{
11-
public IReadOnlyDictionary<string, byte[]> ApplicationState { get; internal set; }
11+
public IReadOnlyDictionary<string, byte[]> ApplicationState { get; set; }
1212

13-
public byte[] RootComponents { get; internal set; }
13+
public byte[] RootComponents { get; set; }
1414

1515
private string GetDebuggerDisplay()
1616
{

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,26 +77,25 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
7777
services.TryAddScoped<AntiforgeryStateProvider, DefaultAntiforgeryStateProvider>();
7878

7979
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
80-
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>(); services.TryAddSingleton<ISystemClock, SystemClock>();
80+
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>();
81+
services.TryAddSingleton<ISystemClock, SystemClock>();
8182
services.TryAddSingleton<CircuitRegistry>();
8283
services.TryAddSingleton<CircuitPersistenceManager>();
8384

8485
// Register the circuit persistence provider conditionally based on HybridCache availability
8586
services.TryAddSingleton<ICircuitPersistenceProvider>(serviceProvider =>
8687
{
87-
var circuitOptions = serviceProvider.GetRequiredService<IOptions<CircuitOptions>>().Value;
88-
if (circuitOptions.HybridPersistenceCache is not null)
88+
var circuitOptions = serviceProvider.GetRequiredService<IOptions<CircuitOptions>>();
89+
if (circuitOptions.Value.HybridPersistenceCache is not null)
8990
{
9091
var logger = serviceProvider.GetRequiredService<ILogger<ICircuitPersistenceProvider>>();
91-
var options = serviceProvider.GetRequiredService<IOptions<CircuitOptions>>();
92-
return new HybridCacheCircuitPersistenceProvider(circuitOptions.HybridPersistenceCache, logger, options);
92+
return new HybridCacheCircuitPersistenceProvider(circuitOptions.Value.HybridPersistenceCache, logger, circuitOptions);
9393
}
9494
else
9595
{
9696
var logger = serviceProvider.GetRequiredService<ILogger<ICircuitPersistenceProvider>>();
97-
var options = serviceProvider.GetRequiredService<IOptions<CircuitOptions>>();
9897
var clock = serviceProvider.GetRequiredService<ISystemClock>();
99-
return new DefaultInMemoryCircuitPersistenceProvider(clock, logger, options);
98+
return new DefaultInMemoryCircuitPersistenceProvider(clock, logger, circuitOptions);
10099
}
101100
});
102101

src/Components/Server/test/Circuits/HybridCacheCircuitPersistenceProviderTest.cs

Lines changed: 34 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.AspNetCore.Components.Server.Circuits;
55
using Microsoft.Extensions.Caching.Hybrid;
6+
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.Logging.Abstractions;
78
using Microsoft.Extensions.Options;
89
using Moq;
@@ -15,43 +16,38 @@ public class HybridCacheCircuitPersistenceProviderTest
1516
public async Task PersistCircuitAsync_StoresCircuitState()
1617
{
1718
// Arrange
18-
var mockHybridCache = new Mock<HybridCache>();
19+
var hybridCache = CreateHybridCache();
1920
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
20-
var persistedState = new PersistedCircuitState();
21-
var provider = CreateProvider(mockHybridCache.Object);
21+
var persistedState = new PersistedCircuitState() {
22+
RootComponents = [1, 2, 3],
23+
ApplicationState = new Dictionary<string, byte[]> {
24+
{ "key1", new byte[] { 4, 5, 6 } },
25+
{ "key2", new byte[] { 7, 8, 9 } }
26+
}
27+
};
28+
var provider = CreateProvider(hybridCache);
2229

2330
// Act
2431
await provider.PersistCircuitAsync(circuitId, persistedState);
2532

2633
// Assert
27-
mockHybridCache.Verify(
28-
c => c.SetAsync(
29-
$"blazor-circuit:{circuitId.Secret}",
30-
persistedState,
31-
It.IsAny<HybridCacheEntryOptions>(),
32-
It.IsAny<IEnumerable<string>>(),
33-
It.IsAny<CancellationToken>()),
34-
Times.Once);
34+
var result = await provider.RestoreCircuitAsync(circuitId);
35+
Assert.NotNull(result);
36+
Assert.Equal(persistedState.RootComponents, result.RootComponents);
37+
Assert.Equal(persistedState.ApplicationState, result.ApplicationState);
3538
}
3639

3740
[Fact]
3841
public async Task RestoreCircuitAsync_ReturnsPersistedState_WhenCircuitExists()
3942
{
4043
// Arrange
41-
var mockHybridCache = new Mock<HybridCache>();
44+
var hybridCache = CreateHybridCache();
4245
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
4346
var persistedState = new PersistedCircuitState();
44-
var provider = CreateProvider(mockHybridCache.Object);
45-
var cacheKey = $"blazor-circuit:{circuitId.Secret}";
46-
mockHybridCache
47-
.Setup(c => c.GetOrCreateAsync(
48-
cacheKey,
49-
It.IsAny<Func<CancellationToken, ValueTask<PersistedCircuitState>>>(),
50-
It.IsAny<Func<Func<CancellationToken, ValueTask<PersistedCircuitState>>, CancellationToken, ValueTask<PersistedCircuitState>>>(),
51-
It.IsAny<HybridCacheEntryOptions>(),
52-
It.IsAny<IEnumerable<string>>(),
53-
It.IsAny<CancellationToken>()))
54-
.ReturnsAsync(persistedState);
47+
var provider = CreateProvider(hybridCache);
48+
var cacheKey = circuitId.Secret;
49+
50+
await provider.PersistCircuitAsync(circuitId, persistedState);
5551

5652
// Act
5753
var result = await provider.RestoreCircuitAsync(circuitId);
@@ -64,19 +60,10 @@ public async Task RestoreCircuitAsync_ReturnsPersistedState_WhenCircuitExists()
6460
public async Task RestoreCircuitAsync_ReturnsNull_WhenCircuitDoesNotExist()
6561
{
6662
// Arrange
67-
var mockHybridCache = new Mock<HybridCache>();
63+
var hybridCache = CreateHybridCache();
6864
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
69-
var provider = CreateProvider(mockHybridCache.Object);
70-
var cacheKey = $"blazor-circuit:{circuitId.Secret}";
71-
mockHybridCache
72-
.Setup(c => c.GetOrCreateAsync(
73-
cacheKey,
74-
It.IsAny<Func<CancellationToken, ValueTask<PersistedCircuitState>>>(),
75-
It.IsAny<Func<Func<CancellationToken, ValueTask<PersistedCircuitState>>, CancellationToken, ValueTask<PersistedCircuitState>>>(),
76-
It.IsAny<HybridCacheEntryOptions>(),
77-
It.IsAny<IEnumerable<string>>(),
78-
It.IsAny<CancellationToken>()))
79-
.ReturnsAsync((PersistedCircuitState)null);
65+
var provider = CreateProvider(hybridCache);
66+
var cacheKey = circuitId.Secret;
8067

8168
// Act
8269
var result = await provider.RestoreCircuitAsync(circuitId);
@@ -89,133 +76,29 @@ public async Task RestoreCircuitAsync_ReturnsNull_WhenCircuitDoesNotExist()
8976
public async Task RestoreCircuitAsync_RemovesCircuitFromCache()
9077
{
9178
// Arrange
92-
var mockHybridCache = new Mock<HybridCache>();
79+
var hybridCache = CreateHybridCache();
9380
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
9481
var persistedState = new PersistedCircuitState();
95-
var provider = CreateProvider(mockHybridCache.Object);
96-
var cacheKey = $"blazor-circuit:{circuitId.Secret}";
97-
98-
mockHybridCache
99-
.Setup(c => c.GetOrCreateAsync(
100-
cacheKey,
101-
It.IsAny<Func<CancellationToken, ValueTask<PersistedCircuitState>>>(),
102-
It.IsAny<Func<Func<CancellationToken, ValueTask<PersistedCircuitState>>, CancellationToken, ValueTask<PersistedCircuitState>>>(),
103-
It.IsAny<HybridCacheEntryOptions>(),
104-
It.IsAny<IEnumerable<string>>(),
105-
It.IsAny<CancellationToken>()))
106-
.ReturnsAsync(persistedState);
107-
108-
// Act
109-
var result = await provider.RestoreCircuitAsync(circuitId);
110-
111-
// Assert
112-
Assert.Same(persistedState, result);
113-
mockHybridCache.Verify(
114-
c => c.RemoveAsync($"blazor-circuit:{circuitId.Secret}", It.IsAny<CancellationToken>()),
115-
Times.Once);
116-
}
82+
var provider = CreateProvider(hybridCache);
83+
var cacheKey = circuitId.Secret;
11784

118-
[Fact]
119-
public async Task PersistCircuitAsync_HandlesExceptions_GracefullyWithLogging()
120-
{
121-
// Arrange
122-
var mockHybridCache = new Mock<HybridCache>();
123-
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
124-
var persistedState = new PersistedCircuitState();
125-
var provider = CreateProvider(mockHybridCache.Object);
126-
127-
mockHybridCache
128-
.Setup(c => c.SetAsync(
129-
It.IsAny<string>(),
130-
It.IsAny<PersistedCircuitState>(),
131-
It.IsAny<HybridCacheEntryOptions>(),
132-
It.IsAny<IEnumerable<string>>(),
133-
It.IsAny<CancellationToken>()))
134-
.Throws(new InvalidOperationException("Test exception"));
135-
136-
// Act & Assert - should not throw
13785
await provider.PersistCircuitAsync(circuitId, persistedState);
138-
}
139-
140-
[Fact]
141-
public async Task RestoreCircuitAsync_HandlesExceptions_GracefullyWithLogging()
142-
{
143-
// Arrange
144-
var mockHybridCache = new Mock<HybridCache>();
145-
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
146-
var provider = CreateProvider(mockHybridCache.Object);
147-
148-
mockHybridCache
149-
.Setup(c => c.GetOrCreateAsync(
150-
It.IsAny<string>(),
151-
It.IsAny<Func<CancellationToken, ValueTask<PersistedCircuitState>>>(),
152-
It.IsAny<Func<Func<CancellationToken, ValueTask<PersistedCircuitState>>, CancellationToken, ValueTask<PersistedCircuitState>>>(),
153-
It.IsAny<HybridCacheEntryOptions>(),
154-
It.IsAny<IEnumerable<string>>(),
155-
It.IsAny<CancellationToken>()))
156-
.ThrowsAsync(new InvalidOperationException("Test exception"));
15786

15887
// Act
159-
var result = await provider.RestoreCircuitAsync(circuitId);
160-
161-
// Assert - should return null when exception occurs
162-
Assert.Null(result);
163-
}
164-
165-
[Fact]
166-
public async Task PersistCircuitAsync_UsesCorrectCacheKey()
167-
{
168-
// Arrange
169-
var mockHybridCache = new Mock<HybridCache>();
170-
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
171-
var persistedState = new PersistedCircuitState();
172-
var provider = CreateProvider(mockHybridCache.Object);
173-
174-
// Act
175-
await provider.PersistCircuitAsync(circuitId, persistedState);
88+
var result1 = await provider.RestoreCircuitAsync(circuitId);
89+
var result2 = await provider.RestoreCircuitAsync(circuitId);
17690

17791
// Assert
178-
mockHybridCache.Verify(
179-
c => c.SetAsync(
180-
$"blazor-circuit:{circuitId.Secret}",
181-
persistedState,
182-
It.IsAny<HybridCacheEntryOptions>(),
183-
It.IsAny<IEnumerable<string>>(),
184-
It.IsAny<CancellationToken>()),
185-
Times.Once);
92+
Assert.Same(persistedState, result1);
93+
Assert.Null(result2); // Circuit should be removed after first restore
18694
}
18795

188-
[Fact]
189-
public async Task PersistCircuitAsync_UsesCorrectExpirationTime()
96+
private HybridCache CreateHybridCache()
19097
{
191-
// Arrange
192-
var mockHybridCache = new Mock<HybridCache>();
193-
var circuitId = TestCircuitIdFactory.CreateTestFactory().CreateCircuitId();
194-
var persistedState = new PersistedCircuitState();
195-
var customOptions = new CircuitOptions
196-
{
197-
PersistedCircuitInMemoryRetentionPeriod = TimeSpan.FromMinutes(5)
198-
};
199-
var provider = CreateProvider(mockHybridCache.Object, customOptions);
200-
201-
HybridCacheEntryOptions capturedOptions = null;
202-
mockHybridCache
203-
.Setup(c => c.SetAsync(
204-
It.IsAny<string>(),
205-
It.IsAny<PersistedCircuitState>(),
206-
It.IsAny<HybridCacheEntryOptions>(),
207-
It.IsAny<IEnumerable<string>>(),
208-
It.IsAny<CancellationToken>()))
209-
.Callback<string, PersistedCircuitState, HybridCacheEntryOptions, IEnumerable<string>, CancellationToken>(
210-
(key, state, options, tags, ct) => capturedOptions = options);
211-
212-
// Act
213-
await provider.PersistCircuitAsync(circuitId, persistedState);
214-
215-
// Assert
216-
Assert.NotNull(capturedOptions);
217-
Assert.Equal(TimeSpan.FromMinutes(5), capturedOptions.Expiration);
218-
Assert.Equal(TimeSpan.FromMinutes(5), capturedOptions.LocalCacheExpiration);
98+
return new ServiceCollection()
99+
.AddHybridCache().Services
100+
.BuildServiceProvider()
101+
.GetRequiredService<HybridCache>();
219102
}
220103

221104
private static HybridCacheCircuitPersistenceProvider CreateProvider(

src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<Reference Include="Microsoft.AspNetCore.Components.Server" />
99
<Reference Include="Microsoft.AspNetCore.Html.Abstractions" />
1010
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" />
11+
<Reference Include="Microsoft.Extensions.Caching.Hybrid" />
1112
</ItemGroup>
1213

1314
<PropertyGroup>

0 commit comments

Comments
 (0)