Implement hot reload support with cache clearing and registry reset#39
Implement hot reload support with cache clearing and registry reset#39
Conversation
…eset functionality
| catch | ||
| { | ||
| continue; | ||
| } |
| if (reset is null) continue; | ||
|
|
||
| try { reset.Invoke(null, null); } | ||
| catch { /* best-effort; stale registry stays stale */ } |
| foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) | ||
| { | ||
| if (assembly.IsDynamic) continue; | ||
|
|
||
| Type? registryType; | ||
| try | ||
| { | ||
| registryType = assembly.GetType("ExpressiveSharp.Generated.ExpressionRegistry", throwOnError: false); | ||
| } | ||
| catch | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var reset = registryType?.GetMethod("ResetMap", BindingFlags.Static | BindingFlags.NonPublic); | ||
| if (reset is null) continue; | ||
|
|
||
| try { reset.Invoke(null, null); } | ||
| catch { /* best-effort; stale registry stays stale */ } | ||
| } |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds hot reload support to ExpressiveSharp by wiring a .NET MetadataUpdateHandler that clears runtime caches and forces generated expression registries to rebuild after metadata updates, enabling updated [Expressive] bodies to propagate without restarting the app.
Changes:
- Add
ExpressiveHotReloadHandler(assembly-registered) to reset generated registries and clear resolver/replacer caches on hot reload. - Update generated
ExpressionRegistryshape to support aResetMap()entry point (and adjust generator baselines accordingly). - Add unit tests for cache clearing behavior and handler registration; document hot reload support in the README.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs | Adds unit tests validating cache clearing and MetadataUpdateHandler registration. |
| tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt | Updates verified generator output to reflect mutable _map and ResetMap(). |
| tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt | Updates verified generator output to reflect mutable _map and ResetMap(). |
| tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt | Updates verified generator output to reflect mutable _map and ResetMap(). |
| tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt | Updates verified generator output to reflect mutable _map and ResetMap(). |
| src/ExpressiveSharp/Services/ExpressiveResolver.cs | Adds a hot-reload-specific cache invalidation path while preserving scan filter/type name cache; exposes test hooks. |
| src/ExpressiveSharp/Services/ExpressiveReplacer.cs | Adds hot-reload cache invalidation for compiler-generated-closure detection cache. |
| src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs | Introduces MetadataUpdateHandler implementation that resets generated registries and clears caches. |
| src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs | Emits mutable registry map and ResetMap() method for hot reload rebuilds. |
| README.md | Documents hot reload compatibility (dotnet watch). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| writer.WriteLine("private static Dictionary<nint, LambdaExpression> _map = Build();"); | ||
| writer.WriteLine(); | ||
| writer.WriteLine("internal static void ResetMap() => _map = Build();"); |
There was a problem hiding this comment.
ExpressionRegistry’s _map is now reassigned at runtime via ResetMap(), but the generated code publishes the new Dictionary with a plain assignment. If lookups can run concurrently with hot reload, this risks unsafe publication under the .NET memory model (another thread may observe a partially-initialized dictionary instance). Generate ResetMap using Volatile.Write/Interlocked.Exchange and have TryGet read the field via Volatile.Read (or copy to a local via Volatile.Read) before calling TryGetValue.
| public static void ClearCache(Type[]? updatedTypes) | ||
| { | ||
| ResetGeneratedRegistries(); | ||
| ExpressiveResolver.ClearCachesForMetadataUpdate(); | ||
| ExpressiveReplacer.ClearCachesForMetadataUpdate(); | ||
| } |
There was a problem hiding this comment.
ClearCache ignores the updatedTypes payload and instead scans every loaded assembly on each hot reload. In larger apps this can be noticeably expensive (and can repeatedly hit reflection exceptions), even when only one project/type changed. Consider using updatedTypes (when non-null/non-empty) to derive the small set of affected assemblies and only attempt registry resets for those, falling back to a full scan only when the runtime doesn’t provide updated types.
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'ExpressiveSharp Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: 984628e | Previous: ad1718a | Ratio |
|---|---|---|---|
ExpressiveSharp.Benchmarks.GeneratorBenchmarks.RunGenerator_Incremental_NoiseChange(ExpressiveCount: 1) |
54020.761697622445 ns (± 455.1528365685914) |
35737.250479239 ns (± 381.0304886918274) |
1.51 |
This comment was automatically generated by workflow using github-action-benchmark.
Introduce functionality for hot reload that includes cache clearing and a registry reset mechanism. This enhancement allows for dynamic updates to expression trees without requiring a full application restart.