From 984628e547b450375c7c48602205a8d40773de2c Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 17 Apr 2026 01:49:23 +0000 Subject: [PATCH 1/3] feat: implement hot reload support with cache clearing and registry reset functionality --- README.md | 1 + .../Registry/ExpressionRegistryEmitter.cs | 9 ++- .../Services/ExpressiveHotReloadHandler.cs | 48 +++++++++++ .../Services/ExpressiveReplacer.cs | 2 + .../Services/ExpressiveResolver.cs | 18 +++++ ...ethodOverloads_BothRegistered.verified.txt | 4 +- ...ipleExpressives_AllRegistered.verified.txt | 4 +- ...eMethod_RegistryContainsEntry.verified.txt | 4 +- ...roperty_RegistryContainsEntry.verified.txt | 4 +- .../ExpressiveHotReloadHandlerTests.cs | 81 +++++++++++++++++++ 10 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs create mode 100644 tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs diff --git a/README.md b/README.md index 30cb05a6..1541039c 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Mark computed properties and methods with `[Expressive]` to generate companion e | Tuples, index/range, `with`, collection expressions | And more modern C# syntax | | Expression transformers | Built-in + custom `IExpressionTreeTransformer` pipeline | | SQL window functions | ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, SUM/AVG/COUNT/MIN/MAX OVER, LAG/LEAD, FIRST_VALUE/LAST_VALUE/NTH_VALUE with ROWS/RANGE frames (experimental) | +| Hot reload | Compatible with `dotnet watch` — edits to `[Expressive]` bodies propagate to generated expression trees | See the [full documentation](https://efnext.github.io/ExpressiveSharp/guide/introduction) for detailed usage, [reference](https://efnext.github.io/ExpressiveSharp/reference/expressive-attribute), and [recipes](https://efnext.github.io/ExpressiveSharp/recipes/computed-properties). diff --git a/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs b/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs index 2cce32b9..86aa7cf5 100644 --- a/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs +++ b/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs @@ -145,12 +145,15 @@ private static void WriteRegistryEntryStatement(IndentedTextWriter writer, Expre } /// - /// Emits the _map field that lazily builds the registry once at class-load time: - /// private static readonly Dictionary<nint, LambdaExpression> _map = Build(); + /// Emits the _map field plus a ResetMap entry point used by the hot-reload + /// handler to rebuild the map after a metadata update has patched the factory-method IL. + /// Not readonly — the handler reassigns it. /// private static void EmitMapField(IndentedTextWriter writer) { - writer.WriteLine("private static readonly Dictionary _map = Build();"); + writer.WriteLine("private static Dictionary _map = Build();"); + writer.WriteLine(); + writer.WriteLine("internal static void ResetMap() => _map = Build();"); } /// diff --git a/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs b/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs new file mode 100644 index 00000000..169a0a32 --- /dev/null +++ b/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs @@ -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(); + } + + public static void UpdateApplication(Type[]? updatedTypes) => ClearCache(updatedTypes); + + /// + /// Finds every loaded assembly's generated ExpressiveSharp.Generated.ExpressionRegistry + /// class and invokes its ResetMap() method so the next TryGet rebuilds + /// LambdaExpression instances from the hot-reloaded factory IL. + /// + 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; + } + + 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 */ } + } + } +} diff --git a/src/ExpressiveSharp/Services/ExpressiveReplacer.cs b/src/ExpressiveSharp/Services/ExpressiveReplacer.cs index 2674981e..58b6a278 100644 --- a/src/ExpressiveSharp/Services/ExpressiveReplacer.cs +++ b/src/ExpressiveSharp/Services/ExpressiveReplacer.cs @@ -21,6 +21,8 @@ public class ExpressiveReplacer : ExpressionVisitor private static readonly ConditionalWeakTable> _compilerGeneratedClosureCache = new(); + internal static void ClearCachesForMetadataUpdate() => _compilerGeneratedClosureCache.Clear(); + public ExpressiveReplacer(IExpressiveResolver resolver) { _resolver = resolver; diff --git a/src/ExpressiveSharp/Services/ExpressiveResolver.cs b/src/ExpressiveSharp/Services/ExpressiveResolver.cs index ce5f4d7f..27e2c345 100644 --- a/src/ExpressiveSharp/Services/ExpressiveResolver.cs +++ b/src/ExpressiveSharp/Services/ExpressiveResolver.cs @@ -35,6 +35,24 @@ internal static void ResetAllCaches() _assemblyScanFilter = null; } + /// + /// Invalidates cached expression trees so the next lookup rebuilds from the (possibly + /// hot-reloaded) generated factory method. Called from . + /// Preserves _assemblyScanFilter and _typeNameCache — neither goes stale on + /// non-rude edits, and wiping the filter would silently disable a user-configured restriction. + /// + internal static void ClearCachesForMetadataUpdate() + { + _expressionCache.Clear(); + _reflectionCache.Clear(); + _assemblyRegistries.Clear(); + Volatile.Write(ref _lastScannedAssemblyCount, 0); + } + + internal static bool IsExpressionCached(MemberInfo mi) => _expressionCache.ContainsKey(mi); + + internal static Func? GetAssemblyScanFilter() => _assemblyScanFilter; + private static Func? _assemblyScanFilter; /// diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt index 93453bac..8a2b0511 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt @@ -22,7 +22,9 @@ namespace ExpressiveSharp.Generated return map; } - private static readonly Dictionary _map = Build(); + private static Dictionary _map = Build(); + + internal static void ResetMap() => _map = Build(); public static LambdaExpression TryGet(MemberInfo member) { diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt index 2a77d60a..acc299c9 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt @@ -22,7 +22,9 @@ namespace ExpressiveSharp.Generated return map; } - private static readonly Dictionary _map = Build(); + private static Dictionary _map = Build(); + + internal static void ResetMap() => _map = Build(); public static LambdaExpression TryGet(MemberInfo member) { diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt index 804c39ad..8a3579df 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt @@ -21,7 +21,9 @@ namespace ExpressiveSharp.Generated return map; } - private static readonly Dictionary _map = Build(); + private static Dictionary _map = Build(); + + internal static void ResetMap() => _map = Build(); public static LambdaExpression TryGet(MemberInfo member) { diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt index 5126e389..a76f2d62 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt @@ -21,7 +21,9 @@ namespace ExpressiveSharp.Generated return map; } - private static readonly Dictionary _map = Build(); + private static Dictionary _map = Build(); + + internal static void ResetMap() => _map = Build(); public static LambdaExpression TryGet(MemberInfo member) { diff --git a/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs b/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs new file mode 100644 index 00000000..39fc6c9f --- /dev/null +++ b/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs @@ -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(_ => 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() + .ToList(); + + Assert.IsTrue(attributes.Any(a => a.HandlerType == typeof(ExpressiveHotReloadHandler)), + "MetadataUpdateHandlerAttribute for ExpressiveHotReloadHandler not found on ExpressiveSharp assembly."); + } +} From 0f1c37e9880c629d4ffe55118f72276145c9745b Mon Sep 17 00:00:00 2001 From: Koen Date: Sun, 19 Apr 2026 23:49:11 +0000 Subject: [PATCH 2/3] feat: enhance hot reload functionality with cache clearing and assembly selection --- samples/HotReloadSpike/HotReloadSpike.csproj | 15 +++++++++ samples/HotReloadSpike/Models.cs | 20 +++++++++++ samples/HotReloadSpike/Program.cs | 29 ++++++++++++++++ .../Registry/ExpressionRegistryEmitter.cs | 5 +-- .../Services/ExpressiveHotReloadHandler.cs | 33 +++++++++++++++---- ...ethodOverloads_BothRegistered.verified.txt | 2 +- ...ipleExpressives_AllRegistered.verified.txt | 2 +- ...eMethod_RegistryContainsEntry.verified.txt | 2 +- ...roperty_RegistryContainsEntry.verified.txt | 2 +- .../ExpressiveHotReloadHandlerTests.cs | 14 ++++++++ 10 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 samples/HotReloadSpike/HotReloadSpike.csproj create mode 100644 samples/HotReloadSpike/Models.cs create mode 100644 samples/HotReloadSpike/Program.cs diff --git a/samples/HotReloadSpike/HotReloadSpike.csproj b/samples/HotReloadSpike/HotReloadSpike.csproj new file mode 100644 index 00000000..8c606241 --- /dev/null +++ b/samples/HotReloadSpike/HotReloadSpike.csproj @@ -0,0 +1,15 @@ + + + + Exe + false + HotReloadSpike + net8.0 + + + + + + + + diff --git a/samples/HotReloadSpike/Models.cs b/samples/HotReloadSpike/Models.cs new file mode 100644 index 00000000..b1937d47 --- /dev/null +++ b/samples/HotReloadSpike/Models.cs @@ -0,0 +1,20 @@ +using ExpressiveSharp; + +public class Order +{ + public int Id { get; set; } + public string? Tag { get; set; } + public double Price { get; set; } + public int Quantity { get; set; } + + [Expressive] + public double Total => (Price * Quantity) + 99; + + [Expressive] + public string GetGrade() => Price switch + { + >= 100 => "Premium", + >= 50 => "Standard", + _ => "Budget", + }; +} diff --git a/samples/HotReloadSpike/Program.cs b/samples/HotReloadSpike/Program.cs new file mode 100644 index 00000000..476dc073 --- /dev/null +++ b/samples/HotReloadSpike/Program.cs @@ -0,0 +1,29 @@ +using System.Linq.Expressions; +using ExpressiveSharp; + +Console.WriteLine("HotReload spike — edit Models.cs while running and watch output."); +Console.WriteLine("Press Ctrl+C to exit.\n"); + +var i = 0; +while (true) +{ + var orders = new List + { + new() { Id = 1, Tag = "urgent", Price = 120.0, Quantity = 2 }, + new() { Id = 2, Tag = "bulk", Price = 8.0, Quantity = 50 }, + new() { Id = 3, Tag = null, Price = 2000.0, Quantity = 1 }, + }; + Console.WriteLine($"--- tick {i++} @ {DateTime.Now:HH:mm:ss} ---"); + + Expression> totalExpr = o => o.Total; + Console.WriteLine($" Total expanded: {totalExpr.ExpandExpressives()}"); + + Expression> gradeExpr = o => o.GetGrade(); + Console.WriteLine($" GetGrade expanded: {gradeExpr.ExpandExpressives()}"); + + foreach (var o in orders) + Console.WriteLine($" #{o.Id} Total={o.Total,6:F2} Grade={o.GetGrade()}"); + + Console.WriteLine(); + await Task.Delay(2000); +} diff --git a/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs b/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs index 86aa7cf5..30c0a5bc 100644 --- a/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs +++ b/src/ExpressiveSharp.Generator/Registry/ExpressionRegistryEmitter.cs @@ -147,11 +147,12 @@ private static void WriteRegistryEntryStatement(IndentedTextWriter writer, Expre /// /// Emits the _map field plus a ResetMap entry point used by the hot-reload /// handler to rebuild the map after a metadata update has patched the factory-method IL. - /// Not readonly — the handler reassigns it. + /// volatile ensures the new + /// is safely published to concurrent readers on weak-memory architectures. /// private static void EmitMapField(IndentedTextWriter writer) { - writer.WriteLine("private static Dictionary _map = Build();"); + writer.WriteLine("private static volatile Dictionary _map = Build();"); writer.WriteLine(); writer.WriteLine("internal static void ResetMap() => _map = Build();"); } diff --git a/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs b/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs index 169a0a32..cb491659 100644 --- a/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs +++ b/src/ExpressiveSharp/Services/ExpressiveHotReloadHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Reflection.Metadata; @@ -10,7 +11,7 @@ internal static class ExpressiveHotReloadHandler { public static void ClearCache(Type[]? updatedTypes) { - ResetGeneratedRegistries(); + ResetGeneratedRegistries(SelectAffectedAssemblies(updatedTypes)); ExpressiveResolver.ClearCachesForMetadataUpdate(); ExpressiveReplacer.ClearCachesForMetadataUpdate(); } @@ -18,13 +19,33 @@ public static void ClearCache(Type[]? updatedTypes) public static void UpdateApplication(Type[]? updatedTypes) => ClearCache(updatedTypes); /// - /// Finds every loaded assembly's generated ExpressiveSharp.Generated.ExpressionRegistry - /// class and invokes its ResetMap() method so the next TryGet rebuilds - /// LambdaExpression instances from the hot-reloaded factory IL. + /// When the runtime tells us which types changed, use their assemblies directly. + /// Fall back to a full scan only when is null or empty, + /// which the runtime may do for large/unknown change sets. /// - private static void ResetGeneratedRegistries() + private static IEnumerable SelectAffectedAssemblies(Type[]? updatedTypes) { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + if (updatedTypes is { Length: > 0 }) + { + var set = new HashSet(); + foreach (var t in updatedTypes) + { + if (t is not null) set.Add(t.Assembly); + } + return set; + } + + return AppDomain.CurrentDomain.GetAssemblies(); + } + + /// + /// Invokes ResetMap() on each assembly's generated ExpressionRegistry class + /// (when present) so the next TryGet rebuilds LambdaExpression instances + /// from the hot-reloaded factory IL. + /// + private static void ResetGeneratedRegistries(IEnumerable assemblies) + { + foreach (var assembly in assemblies) { if (assembly.IsDynamic) continue; diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt index 8a2b0511..8fe54561 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MethodOverloads_BothRegistered.verified.txt @@ -22,7 +22,7 @@ namespace ExpressiveSharp.Generated return map; } - private static Dictionary _map = Build(); + private static volatile Dictionary _map = Build(); internal static void ResetMap() => _map = Build(); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt index acc299c9..64aab713 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.MultipleExpressives_AllRegistered.verified.txt @@ -22,7 +22,7 @@ namespace ExpressiveSharp.Generated return map; } - private static Dictionary _map = Build(); + private static volatile Dictionary _map = Build(); internal static void ResetMap() => _map = Build(); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt index 8a3579df..3ff6151d 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt @@ -21,7 +21,7 @@ namespace ExpressiveSharp.Generated return map; } - private static Dictionary _map = Build(); + private static volatile Dictionary _map = Build(); internal static void ResetMap() => _map = Build(); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt index a76f2d62..56bdc827 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt @@ -21,7 +21,7 @@ namespace ExpressiveSharp.Generated return map; } - private static Dictionary _map = Build(); + private static volatile Dictionary _map = Build(); internal static void ResetMap() => _map = Build(); diff --git a/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs b/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs index 39fc6c9f..d1149146 100644 --- a/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs +++ b/tests/ExpressiveSharp.Tests/Services/ExpressiveHotReloadHandlerTests.cs @@ -62,6 +62,20 @@ public void ClearCache_WithNullAndEmptyAndPopulatedArrays_DoesNotThrow() ExpressiveHotReloadHandler.ClearCache([typeof(Product)]); } + [TestMethod] + public void ClearCache_WithUpdatedTypes_ClearsResolverCache() + { + var mi = typeof(Product).GetProperty(nameof(Product.Total))!; + var resolver = new ExpressiveResolver(); + + _ = resolver.FindGeneratedExpression(mi); + Assert.IsTrue(ExpressiveResolver.IsExpressionCached(mi)); + + ExpressiveHotReloadHandler.ClearCache([typeof(Product)]); + + Assert.IsFalse(ExpressiveResolver.IsExpressionCached(mi)); + } + [TestMethod] public void UpdateApplication_WithNull_DoesNotThrow() { From 12e9ae00c315e7031fd4b0a2e2000b46234018f2 Mon Sep 17 00:00:00 2001 From: Koen Date: Sun, 19 Apr 2026 23:49:33 +0000 Subject: [PATCH 3/3] Removed hot reload spike --- samples/HotReloadSpike/HotReloadSpike.csproj | 15 ---------- samples/HotReloadSpike/Models.cs | 20 -------------- samples/HotReloadSpike/Program.cs | 29 -------------------- 3 files changed, 64 deletions(-) delete mode 100644 samples/HotReloadSpike/HotReloadSpike.csproj delete mode 100644 samples/HotReloadSpike/Models.cs delete mode 100644 samples/HotReloadSpike/Program.cs diff --git a/samples/HotReloadSpike/HotReloadSpike.csproj b/samples/HotReloadSpike/HotReloadSpike.csproj deleted file mode 100644 index 8c606241..00000000 --- a/samples/HotReloadSpike/HotReloadSpike.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - false - HotReloadSpike - net8.0 - - - - - - - - diff --git a/samples/HotReloadSpike/Models.cs b/samples/HotReloadSpike/Models.cs deleted file mode 100644 index b1937d47..00000000 --- a/samples/HotReloadSpike/Models.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ExpressiveSharp; - -public class Order -{ - public int Id { get; set; } - public string? Tag { get; set; } - public double Price { get; set; } - public int Quantity { get; set; } - - [Expressive] - public double Total => (Price * Quantity) + 99; - - [Expressive] - public string GetGrade() => Price switch - { - >= 100 => "Premium", - >= 50 => "Standard", - _ => "Budget", - }; -} diff --git a/samples/HotReloadSpike/Program.cs b/samples/HotReloadSpike/Program.cs deleted file mode 100644 index 476dc073..00000000 --- a/samples/HotReloadSpike/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq.Expressions; -using ExpressiveSharp; - -Console.WriteLine("HotReload spike — edit Models.cs while running and watch output."); -Console.WriteLine("Press Ctrl+C to exit.\n"); - -var i = 0; -while (true) -{ - var orders = new List - { - new() { Id = 1, Tag = "urgent", Price = 120.0, Quantity = 2 }, - new() { Id = 2, Tag = "bulk", Price = 8.0, Quantity = 50 }, - new() { Id = 3, Tag = null, Price = 2000.0, Quantity = 1 }, - }; - Console.WriteLine($"--- tick {i++} @ {DateTime.Now:HH:mm:ss} ---"); - - Expression> totalExpr = o => o.Total; - Console.WriteLine($" Total expanded: {totalExpr.ExpandExpressives()}"); - - Expression> gradeExpr = o => o.GetGrade(); - Console.WriteLine($" GetGrade expanded: {gradeExpr.ExpandExpressives()}"); - - foreach (var o in orders) - Console.WriteLine($" #{o.Id} Total={o.Total,6:F2} Grade={o.GetGrade()}"); - - Console.WriteLine(); - await Task.Delay(2000); -}