-
Notifications
You must be signed in to change notification settings - Fork 0
Implement hot reload support with cache clearing and registry reset #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| using System; | ||
| using System.Reflection; | ||
| using System.Reflection.Metadata; | ||
|
|
||
| [assembly: MetadataUpdateHandler(typeof(ExpressiveSharp.Services.ExpressiveHotReloadHandler))] | ||
|
|
||
| namespace ExpressiveSharp.Services; | ||
|
|
||
| internal static class ExpressiveHotReloadHandler | ||
| { | ||
| public static void ClearCache(Type[]? updatedTypes) | ||
| { | ||
| ResetGeneratedRegistries(); | ||
| ExpressiveResolver.ClearCachesForMetadataUpdate(); | ||
| ExpressiveReplacer.ClearCachesForMetadataUpdate(); | ||
| } | ||
|
Comment on lines
+11
to
+16
|
||
|
|
||
| public static void UpdateApplication(Type[]? updatedTypes) => ClearCache(updatedTypes); | ||
|
|
||
| /// <summary> | ||
| /// Finds every loaded assembly's generated <c>ExpressiveSharp.Generated.ExpressionRegistry</c> | ||
| /// class and invokes its <c>ResetMap()</c> method so the next <c>TryGet</c> rebuilds | ||
| /// <c>LambdaExpression</c> instances from the hot-reloaded factory IL. | ||
| /// </summary> | ||
| private static void ResetGeneratedRegistries() | ||
| { | ||
| foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) | ||
| { | ||
| if (assembly.IsDynamic) continue; | ||
|
|
||
| Type? registryType; | ||
| try | ||
| { | ||
| registryType = assembly.GetType("ExpressiveSharp.Generated.ExpressionRegistry", throwOnError: false); | ||
| } | ||
| catch | ||
| { | ||
| continue; | ||
| } | ||
|
Comment on lines
+36
to
+39
|
||
|
|
||
| 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 */ } | ||
|
|
||
| } | ||
|
Comment on lines
+27
to
+46
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| using System.Reflection; | ||
| using System.Reflection.Metadata; | ||
| using ExpressiveSharp.Services; | ||
| using ExpressiveSharp.Tests.TestFixtures; | ||
|
|
||
| namespace ExpressiveSharp.Tests.Services; | ||
|
|
||
| [TestClass] | ||
| public class ExpressiveHotReloadHandlerTests | ||
| { | ||
| [TestMethod] | ||
| public void ClearCache_AfterResolve_RemovesMemberFromCache() | ||
| { | ||
| var mi = typeof(Product).GetProperty(nameof(Product.Total))!; | ||
| var resolver = new ExpressiveResolver(); | ||
|
|
||
| _ = resolver.FindGeneratedExpression(mi); | ||
| Assert.IsTrue(ExpressiveResolver.IsExpressionCached(mi)); | ||
|
|
||
| ExpressiveHotReloadHandler.ClearCache(null); | ||
|
|
||
| Assert.IsFalse(ExpressiveResolver.IsExpressionCached(mi)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ClearCache_PreservesAssemblyScanFilter() | ||
| { | ||
| var sentinel = new Func<Assembly, bool>(_ => true); | ||
| ExpressiveResolver.SetAssemblyScanFilter(sentinel); | ||
| try | ||
| { | ||
| ExpressiveHotReloadHandler.ClearCache(null); | ||
|
|
||
| Assert.AreSame(sentinel, ExpressiveResolver.GetAssemblyScanFilter()); | ||
| } | ||
| finally | ||
| { | ||
| ExpressiveResolver.SetAssemblyScanFilter(null); | ||
| } | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ClearCache_RebuildReturnsEquivalentExpression() | ||
| { | ||
| var mi = typeof(Product).GetProperty(nameof(Product.Total))!; | ||
| var resolver = new ExpressiveResolver(); | ||
|
|
||
| var before = resolver.FindGeneratedExpression(mi).ToString(); | ||
|
|
||
| ExpressiveHotReloadHandler.ClearCache(null); | ||
|
|
||
| var after = resolver.FindGeneratedExpression(mi).ToString(); | ||
|
|
||
| Assert.AreEqual(before, after); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ClearCache_WithNullAndEmptyAndPopulatedArrays_DoesNotThrow() | ||
| { | ||
| ExpressiveHotReloadHandler.ClearCache(null); | ||
| ExpressiveHotReloadHandler.ClearCache([]); | ||
| ExpressiveHotReloadHandler.ClearCache([typeof(Product)]); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void UpdateApplication_WithNull_DoesNotThrow() | ||
| { | ||
| ExpressiveHotReloadHandler.UpdateApplication(null); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void Assembly_RegistersExpressiveHotReloadHandler() | ||
| { | ||
| var attributes = typeof(ExpressiveResolver).Assembly | ||
| .GetCustomAttributes<MetadataUpdateHandlerAttribute>() | ||
| .ToList(); | ||
|
|
||
| Assert.IsTrue(attributes.Any(a => a.HandlerType == typeof(ExpressiveHotReloadHandler)), | ||
| "MetadataUpdateHandlerAttribute for ExpressiveHotReloadHandler not found on ExpressiveSharp assembly."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExpressionRegistry’s_mapis now reassigned at runtime viaResetMap(), but the generated code publishes the newDictionarywith 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). GenerateResetMapusingVolatile.Write/Interlocked.Exchangeand haveTryGetread the field viaVolatile.Read(or copy to a local viaVolatile.Read) before callingTryGetValue.