|
4 | 4 | using System.Collections.Concurrent;
|
5 | 5 | using System.Diagnostics;
|
6 | 6 | using System.Globalization;
|
| 7 | +using System.Reflection; |
7 | 8 | using System.Runtime.ExceptionServices;
|
8 | 9 | using Microsoft.AspNetCore.Components.CompilerServices;
|
9 | 10 | using Microsoft.AspNetCore.Components.HotReload;
|
@@ -5027,6 +5028,40 @@ public async Task DisposingRenderer_UnsubsribesFromHotReloadManager()
|
5027 | 5028 | Assert.False(hotReloadManager.IsSubscribedTo);
|
5028 | 5029 | }
|
5029 | 5030 |
|
| 5031 | + [Fact] |
| 5032 | + public async Task HotReload_ReRenderPreservesAsyncLocalValues() |
| 5033 | + { |
| 5034 | + await using var renderer = new TestRenderer(); |
| 5035 | + |
| 5036 | + var hotReloadManager = new HotReloadManager { MetadataUpdateSupported = true }; |
| 5037 | + renderer.HotReloadManager = hotReloadManager; |
| 5038 | + HotReloadManager.Default.MetadataUpdateSupported = true; |
| 5039 | + |
| 5040 | + var component = new AsyncLocalCaptureComponent(); |
| 5041 | + |
| 5042 | + // Establish AsyncLocal value before registering hot reload handler / rendering. |
| 5043 | + ServiceAccessor.TestAsyncLocal.Value = "AmbientValue"; |
| 5044 | + |
| 5045 | + var componentId = renderer.AssignRootComponentId(component); |
| 5046 | + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId)); |
| 5047 | + |
| 5048 | + // Sanity: initial render should not have captured a hot-reload value yet. |
| 5049 | + Assert.Null(component.HotReloadValue); |
| 5050 | + |
| 5051 | + // Simulate hot reload delta applied from a fresh thread (different ExecutionContext) so the AsyncLocal value is lost. |
| 5052 | + var expected = ServiceAccessor.TestAsyncLocal.Value; |
| 5053 | + var thread = new Thread(() => |
| 5054 | + { |
| 5055 | + // Simulate environment where the ambient value is not present on the hot reload thread. |
| 5056 | + ServiceAccessor.TestAsyncLocal.Value = null; |
| 5057 | + hotReloadManager.TriggerOnDeltaApplied(); |
| 5058 | + }); |
| 5059 | + thread.Start(); |
| 5060 | + thread.Join(); |
| 5061 | + |
| 5062 | + Assert.Equal(expected, component.HotReloadValue); |
| 5063 | + } |
| 5064 | + |
5030 | 5065 | [Fact]
|
5031 | 5066 | public void ThrowsForUnknownRenderMode_OnComponentType()
|
5032 | 5067 | {
|
@@ -5180,6 +5215,34 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
5180 | 5215 | => Task.CompletedTask;
|
5181 | 5216 | }
|
5182 | 5217 |
|
| 5218 | + private class ServiceAccessor |
| 5219 | + { |
| 5220 | + public static AsyncLocal<string> TestAsyncLocal = new AsyncLocal<string>(); |
| 5221 | + } |
| 5222 | + |
| 5223 | + private class AsyncLocalCaptureComponent : IComponent |
| 5224 | + { |
| 5225 | + private bool _initialized; |
| 5226 | + private RenderHandle _renderHandle; |
| 5227 | + public string HotReloadValue { get; private set; } |
| 5228 | + |
| 5229 | + public void Attach(RenderHandle renderHandle) => _renderHandle = renderHandle; |
| 5230 | + |
| 5231 | + public Task SetParametersAsync(ParameterView parameters) |
| 5232 | + { |
| 5233 | + if (!_initialized) |
| 5234 | + { |
| 5235 | + _initialized = true; // First (normal) render, don't capture. |
| 5236 | + } |
| 5237 | + else |
| 5238 | + { |
| 5239 | + // Hot reload re-render path. |
| 5240 | + HotReloadValue = ServiceAccessor.TestAsyncLocal.Value; |
| 5241 | + } |
| 5242 | + return Task.CompletedTask; |
| 5243 | + } |
| 5244 | + } |
| 5245 | + |
5183 | 5246 | private class TestComponent : IComponent, IDisposable
|
5184 | 5247 | {
|
5185 | 5248 | private RenderHandle _renderHandle;
|
|
0 commit comments