Skip to content

Commit

Permalink
Added support for hot reload (#71)
Browse files Browse the repository at this point in the history
Fixes #4
  • Loading branch information
DamianEdwards authored Dec 14, 2024
1 parent 037b681 commit 33836f5
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

<PropertyGroup>
<PublishAot>true</PublishAot>
<!-- Startup hooks are disabled when publishing aot/trimmed but are required for Hot Reload to work so enabling it here -->
<StartupHookSupport Condition="'$(Configuration)' == 'Debug'">true</StartupHookSupport>
<!--<PublishTrimmed>true</PublishTrimmed>-->
<!--<PublishSingleFile>true</PublishSingleFile>-->
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
Expand Down
51 changes: 51 additions & 0 deletions src/RazorSlices/HotReloadService.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A service that provides hot reload functionality for Razor Slices.
/// See https://learn.microsoft.com/dotnet/api/system.reflection.metadata.metadataupdatehandlerattribute for more information.
/// </summary>
internal sealed class HotReloadService
{
public static event Action<Type[]?>? ClearCacheEvent;
public static event Action<Type[]?>? 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<MetadataUpdateOriginalTypeAttribute>();
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;
}
}
43 changes: 36 additions & 7 deletions src/RazorSlices/SliceDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace RazorSlices;
Expand All @@ -11,6 +12,8 @@ namespace RazorSlices;
/// </remarks>
public class SliceDefinition
{
private readonly Type _originalSliceType;

/// <summary>
/// Creates a new instance of <see cref="SliceDefinition"/>.
/// </summary>
Expand All @@ -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");
Expand All @@ -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");
}
}

/// <summary>
/// Gets the type of the slice.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public Type SliceType { get; }
public Type SliceType { get; private set; }

/// <summary>
/// Gets whether this slice has a model.
/// </summary>
public bool HasModel { get; }
public bool HasModel { get; private set; }

/// <summary>
/// Gets the information about the model property for the slice if it has a model.
/// </summary>
public PropertyInfo? ModelProperty { get; }
public PropertyInfo? ModelProperty { get; private set; }

/// <summary>
/// Gets the model type for the slice if it has a model.
/// </summary>
public Type? ModelType { get; }
public Type? ModelType { get; private set; }

/// <summary>
/// Gets details of the injectable properties for the slice.
/// </summary>
public (bool Any, PropertyInfo[] Nullable, PropertyInfo[] NonNullable) InjectableProperties { get; }
public (bool Any, PropertyInfo[] Nullable, PropertyInfo[] NonNullable) InjectableProperties { get; private set; }

/// <summary>
/// Gets the factory delegate for creating instances of the slice.
/// </summary>
public Delegate Factory { get; }
public Delegate Factory { get; private set; }

/// <summary>
/// Creates a new instance of the slice this definition represents.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

0 comments on commit 33836f5

Please sign in to comment.