diff --git a/samples/RazorSlices.Samples.WebApp/RazorSlices.Samples.WebApp.csproj b/samples/RazorSlices.Samples.WebApp/RazorSlices.Samples.WebApp.csproj index da4d139..6185eb5 100644 --- a/samples/RazorSlices.Samples.WebApp/RazorSlices.Samples.WebApp.csproj +++ b/samples/RazorSlices.Samples.WebApp/RazorSlices.Samples.WebApp.csproj @@ -9,6 +9,8 @@ true + + true true diff --git a/src/RazorSlices/HotReloadService.cs b/src/RazorSlices/HotReloadService.cs new file mode 100644 index 0000000..0b95809 --- /dev/null +++ b/src/RazorSlices/HotReloadService.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Metadata; +using System.Runtime.CompilerServices; + +[assembly: MetadataUpdateHandler(typeof(RazorSlices.HotReloadService))] + +namespace RazorSlices; + +/// +/// A service that provides hot reload functionality for Razor Slices. +/// See https://learn.microsoft.com/dotnet/api/system.reflection.metadata.metadataupdatehandlerattribute for more information. +/// +internal sealed class HotReloadService +{ + public static event Action? ClearCacheEvent; + public static event Action? UpdateApplicationEvent; + + public static void ClearCache(Type[]? changedTypes) + { + Debug.WriteLine($"{nameof(HotReloadService)}.{nameof(ClearCache)} invoked with {changedTypes?.Length ?? 0} changed types."); + ClearCacheEvent?.Invoke(changedTypes); + } + + public static void UpdateApplication(Type[]? changedTypes) + { + Debug.WriteLine($"{nameof(HotReloadService)}.{nameof(UpdateApplication)} invoked with {changedTypes?.Length ?? 0} changed types."); + UpdateApplicationEvent?.Invoke(changedTypes); + } + + public static bool TryGetUpdatedType(Type[]? changedTypes, Type originalType, [NotNullWhen(true)] out Type? updatedType) + { + if (changedTypes is not null) + { + foreach (var type in changedTypes) + { + var originalTypeAttribute = type.GetCustomAttribute(); + if (originalTypeAttribute?.OriginalType == originalType) + { + updatedType = type; + Debug.WriteLine($"Type '{originalType.Name}' was replaced with type '{updatedType.Name}' by Hot Reload"); + return true; + } + } + } + + updatedType = null; + return false; + } +} diff --git a/src/RazorSlices/SliceDefinition.cs b/src/RazorSlices/SliceDefinition.cs index f8f5409..cda4777 100644 --- a/src/RazorSlices/SliceDefinition.cs +++ b/src/RazorSlices/SliceDefinition.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace RazorSlices; @@ -11,6 +12,8 @@ namespace RazorSlices; /// public class SliceDefinition { + private readonly Type _originalSliceType; + /// /// Creates a new instance of . /// @@ -22,6 +25,18 @@ public SliceDefinition( { ArgumentNullException.ThrowIfNull(sliceType); + HotReloadService.ClearCacheEvent += ReplaceSliceType; + + _originalSliceType = sliceType; + Initialize(sliceType); + Factory = RazorSliceFactory.GetSliceFactory(this); + } + + [MemberNotNull(nameof(SliceType))] + private void Initialize( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type sliceType) + { SliceType = sliceType; HasModel = RazorSliceFactory.IsModelSlice(SliceType); ModelProperty = SliceType.GetProperty("Model"); @@ -30,36 +45,50 @@ public SliceDefinition( Factory = RazorSliceFactory.GetSliceFactory(this); } + private void ReplaceSliceType(Type[]? changedTypes) + { + var started = Stopwatch.GetTimestamp(); + + if (HotReloadService.TryGetUpdatedType(changedTypes, _originalSliceType, out var updatedSliceType)) + { + Debug.Write($"Hot reloading slice of type '{_originalSliceType.Name}'... "); + + Initialize(updatedSliceType); + + Debug.WriteLine($"done! It took {Stopwatch.GetElapsedTime(started).TotalMilliseconds}ms"); + } + } + /// /// Gets the type of the slice. /// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - public Type SliceType { get; } + public Type SliceType { get; private set; } /// /// Gets whether this slice has a model. /// - public bool HasModel { get; } + public bool HasModel { get; private set; } /// /// Gets the information about the model property for the slice if it has a model. /// - public PropertyInfo? ModelProperty { get; } + public PropertyInfo? ModelProperty { get; private set; } /// /// Gets the model type for the slice if it has a model. /// - public Type? ModelType { get; } + public Type? ModelType { get; private set; } /// /// Gets details of the injectable properties for the slice. /// - public (bool Any, PropertyInfo[] Nullable, PropertyInfo[] NonNullable) InjectableProperties { get; } + public (bool Any, PropertyInfo[] Nullable, PropertyInfo[] NonNullable) InjectableProperties { get; private set; } /// /// Gets the factory delegate for creating instances of the slice. /// - public Delegate Factory { get; } + public Delegate Factory { get; private set; } /// /// Creates a new instance of the slice this definition represents. diff --git a/tests/RazorSlices.Samples.WebApp.PublishTests/PublishSample.cs b/tests/RazorSlices.Samples.WebApp.PublishTests/PublishSample.cs index e9c00ad..30f7642 100644 --- a/tests/RazorSlices.Samples.WebApp.PublishTests/PublishSample.cs +++ b/tests/RazorSlices.Samples.WebApp.PublishTests/PublishSample.cs @@ -7,13 +7,14 @@ public class PublishSample const string ProjectName = "RazorSlices.Samples.WebApp"; [Theory] - [InlineData(PublishScenario.Default)] + [InlineData(PublishScenario.Default, "net8.0")] + [InlineData(PublishScenario.Default, "net9.0")] //[InlineData(PublishScenario.Trimmed)] //[InlineData(PublishScenario.AOT)] - public void Publish(PublishScenario publishScenario) + public void Publish(PublishScenario publishScenario, string tfm) { var projectBuilder = new ProjectBuilder(ProjectName, publishScenario); - projectBuilder.Publish("net8.0"); + projectBuilder.Publish(tfm); Assert.DoesNotContain("warning", projectBuilder.PublishResult?.Output, StringComparison.OrdinalIgnoreCase); }