|
| 1 | +using System.Linq.Expressions; |
| 2 | +using System.Reflection; |
| 3 | +using ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure; |
| 4 | +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; |
| 5 | +using ExpressiveSharp.Services; |
| 6 | +using Microsoft.EntityFrameworkCore; |
| 7 | +using Microsoft.VisualStudio.TestTools.UnitTesting; |
| 8 | + |
| 9 | +namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Tests.Sqlite; |
| 10 | + |
| 11 | +[TestClass] |
| 12 | +[DoNotParallelize] |
| 13 | +public class HotReloadTests : EFCoreTestBase |
| 14 | +{ |
| 15 | + protected override IAsyncDisposable CreateContextHandle(out DbContext context) |
| 16 | + { |
| 17 | + var handle = TestContextFactories.CreateSqlite(); |
| 18 | + context = handle.Context; |
| 19 | + return handle; |
| 20 | + } |
| 21 | + |
| 22 | + [TestInitialize] |
| 23 | + public Task SeedHotReloadData() => Context.SeedStoreAsync(); |
| 24 | + |
| 25 | + [TestMethod] |
| 26 | + public async Task HotReload_NewExpressiveBody_FlowsThroughToSqlAndResults() |
| 27 | + { |
| 28 | + var baselineSql = Context.Set<Order>().Where(o => o.Total > 200).ToQueryString(); |
| 29 | + var baselineTotals = await Context.Set<Order>().Select(o => o.Total).OrderBy(t => t).ToListAsync(); |
| 30 | + CollectionAssert.AreEqual(new[] { 30.0, 240.0, 250.0, 1500.0 }, baselineTotals); |
| 31 | + |
| 32 | + var (registryType, mapField, totalKey) = HotReloadRegistry.Locate(typeof(Order), nameof(Order.Total)); |
| 33 | + var map = (IDictionary<nint, LambdaExpression>)mapField.GetValue(null)!; |
| 34 | + Expression<Func<Order, double>> reloaded = o => o.Price * o.Quantity * 2; |
| 35 | + |
| 36 | + try |
| 37 | + { |
| 38 | + map[totalKey] = reloaded; |
| 39 | + HotReloadRegistry.ClearResolverCaches(); |
| 40 | + |
| 41 | + var reloadedSql = Context.Set<Order>().Where(o => o.Total > 200).ToQueryString(); |
| 42 | + Assert.AreNotEqual(baselineSql, reloadedSql, |
| 43 | + "ExpressiveQueryCompiler did not pick up the reloaded body — SQL is unchanged."); |
| 44 | + |
| 45 | + var reloadedTotals = await Context.Set<Order>().Select(o => o.Total).OrderBy(t => t).ToListAsync(); |
| 46 | + CollectionAssert.AreEqual(new[] { 60.0, 480.0, 500.0, 3000.0 }, reloadedTotals); |
| 47 | + } |
| 48 | + finally |
| 49 | + { |
| 50 | + HotReloadRegistry.Reset(registryType); |
| 51 | + HotReloadRegistry.ClearResolverCaches(); |
| 52 | + } |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +internal static class HotReloadRegistry |
| 57 | +{ |
| 58 | + public static (Type RegistryType, FieldInfo MapField, nint MemberKey) Locate(Type declaringType, string propertyName) |
| 59 | + { |
| 60 | + var registryType = declaringType.Assembly.GetType("ExpressiveSharp.Generated.ExpressionRegistry") |
| 61 | + ?? throw new InvalidOperationException( |
| 62 | + $"Generated ExpressionRegistry not found in {declaringType.Assembly.GetName().Name}."); |
| 63 | + var mapField = registryType.GetField("_map", BindingFlags.Static | BindingFlags.NonPublic) |
| 64 | + ?? throw new InvalidOperationException("ExpressionRegistry._map not found."); |
| 65 | + var key = declaringType.GetProperty(propertyName)!.GetMethod!.MethodHandle.Value; |
| 66 | + return (registryType, mapField, key); |
| 67 | + } |
| 68 | + |
| 69 | + public static void Reset(Type registryType) |
| 70 | + { |
| 71 | + var reset = registryType.GetMethod("ResetMap", BindingFlags.Static | BindingFlags.NonPublic)!; |
| 72 | + reset.Invoke(null, null); |
| 73 | + } |
| 74 | + |
| 75 | + public static void ClearResolverCaches() |
| 76 | + { |
| 77 | + var method = typeof(ExpressiveResolver).GetMethod( |
| 78 | + "ClearCachesForMetadataUpdate", |
| 79 | + BindingFlags.Static | BindingFlags.NonPublic)!; |
| 80 | + method.Invoke(null, null); |
| 81 | + } |
| 82 | +} |
0 commit comments