Skip to content

Use MetadataUpdateHandlerAttribute in Blazor #31817

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

Merged
3 commits merged into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions eng/Workarounds.targets
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
<!-- Use this file to workaround issues. List the issue tracking the item to fix so we can remove the workaround when the issue is resolved. -->
<Project>
<PropertyGroup>
<DefaultNetCoreTargetFrameworkIdentifier>$([MSBuild]::GetTargetFrameworkIdentifier('$(DefaultNetCoreTargetFramework)'))</DefaultNetCoreTargetFrameworkIdentifier>
<DefaultNetCoreTargetFrameworkVersion>v$([MSBuild]::GetTargetFrameworkVersion('$(DefaultNetCoreTargetFramework)', 2))</DefaultNetCoreTargetFrameworkVersion>

<ProjectTargetFrameworkIdentifier>$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))</ProjectTargetFrameworkIdentifier>
<ProjectTargetFrameworkVersion>v$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)', 2))</ProjectTargetFrameworkVersion>
</PropertyGroup>

<ItemGroup>
<!-- Reference base shared framework at incoming dependency flow version, not bundled sdk version. -->
<!--
Reference base shared framework at incoming dependency flow version, not bundled sdk version.
Apply this to all projects that target the default tfm (e.g. net6.0) or a rid-based variant of it (e.g. net6.0-windows)
-->
<FrameworkReference
Update="Microsoft.NETCore.App"
Condition=" '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' AND '$(TargetLatestDotNetRuntime)' != 'false' "
Condition=" (('$(ProjectTargetFrameworkIdentifier)' == '$(DefaultNetCoreTargetFrameworkIdentifier)') AND '$(DefaultNetCoreTargetFrameworkVersion)' == '$(ProjectTargetFrameworkVersion)') AND '$(TargetLatestDotNetRuntime)' != 'false' "
RuntimeFrameworkVersion="$(MicrosoftNETCoreAppRuntimeVersion)"
TargetingPackVersion="$(MicrosoftNETCoreAppRefVersion)" />
</ItemGroup>
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Components/src/HotReload/HotReloadManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

using System;
using System.Reflection;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components.HotReload;

[assembly: AssemblyMetadata("ReceiveHotReloadDeltaNotification", "Microsoft.AspNetCore.Components.HotReload.HotReloadManager")]
[assembly: MetadataUpdateHandler(typeof(HotReloadManager))]

namespace Microsoft.AspNetCore.Components.HotReload
{
Expand All @@ -16,5 +19,7 @@ public static void DeltaApplied()
{
OnDeltaApplied?.Invoke();
}

public static void OnAfterUpdate(Type[]? _) => OnDeltaApplied?.Invoke();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.HotReload;
Expand All @@ -25,6 +26,7 @@ public static class WebAssemblyHotReload
{
private static readonly ConcurrentDictionary<Guid, List<(byte[] metadataDelta, byte[] ilDelta)>> _deltas = new();
private static readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
private static (List<Action<Type[]?>> BeforeUpdates, List<Action<Type[]?>> AfterUpdates)? _handlerActions;

static WebAssemblyHotReload()
{
Expand All @@ -50,7 +52,7 @@ static WebAssemblyHotReload()
// A delta for this specific Module exists and we haven't called ApplyUpdate on this instance of Assembly as yet.
foreach (var (metadataDelta, ilDelta) in CollectionsMarshal.AsSpan(result))
{
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(loadedAssembly, metadataDelta, ilDelta, ReadOnlySpan<byte>.Empty);
ApplyUpdate(loadedAssembly, metadataDelta, ilDelta);
}
}
};
Expand Down Expand Up @@ -80,7 +82,7 @@ public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDel

if (assembly is not null)
{
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan<byte>.Empty);
ApplyUpdate(assembly, metadataDelta, ilDeta);
_appliedAssemblies.TryAdd(assembly, assembly);
}

Expand All @@ -95,9 +97,87 @@ public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDel
(metadataDelta, ilDeta)
};
}
}

private static void ApplyUpdate(Assembly assembly, byte[] metadataDelta, byte[] ilDeta)
{
_handlerActions ??= GetMetadataUpdateHandlerActions();
var (beforeUpdates, afterUpdates) = _handlerActions.Value;

beforeUpdates.ForEach(a => a(null));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I understand correctly beforeUpdates and afterUpdates are handlers that can be invoked before and after deltas are applied. What are some scenarios where they are used?

Also -- why does this take a null value instead of just being a parameterless method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are some scenarios where they are used?

e.g. to clear corelib's reflection cache, to clear JsonSerializer's cache of reflection data, to refresh a binding in a UI, etc.

why does this take a null value instead of just being a parameterless method?

The plan is for the agent to pass an array of the types being updated, if available, but we don't have that information yet. The pattern will likely evolve over the next few months as we find out more about what various handlers need.

System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan<byte>.Empty);
afterUpdates.ForEach(a => a(null));
}

private static (List<Action<Type[]?>> BeforeUpdates, List<Action<Type[]?>> AfterUpdates) GetMetadataUpdateHandlerActions()
{
var beforeUpdates = new List<Action<Type[]?>>();
var afterUpdates = new List<Action<Type[]?>>();

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var attribute in assembly.GetCustomAttributes<MetadataUpdateHandlerAttribute>())
{
var handlerType = attribute.HandlerType;

var methodFound = false;
if (GetUpdateMethod(handlerType, "BeforeUpdate") is MethodInfo beforeUpdate)
{
beforeUpdates.Add(CreateAction(beforeUpdate));
methodFound = true;
}

if (GetUpdateMethod(handlerType, "AfterUpdate") is MethodInfo afterUpdate)
{
afterUpdates.Add(CreateAction(afterUpdate));
methodFound = true;
}

if (!methodFound)
{
Debug.WriteLine($"No BeforeUpdate or AfterUpdate method found on '{handlerType}'.");
}

static Action<Type[]?> CreateAction(MethodInfo update)
{
var action = update.CreateDelegate<Action<Type[]?>>();
return types =>
{
try
{
action(types);
}
catch (Exception ex)
{
Debug.WriteLine($"Exception from '{action}': {ex}");
}
};
}
}
}

static MethodInfo? GetUpdateMethod(Type handlerType, string name)
{
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
var updateMethod = handlerType.GetMethod(name, bindingFlags, new[] { typeof(Type[]) });
if (updateMethod is not null)
{
return updateMethod;
}

var methods = handlerType.GetMethods(bindingFlags)
.Where(m => m.Name == name)
.ToArray();

if (methods.Length > 0)
{
Debug.WriteLine($"MetadataUpdateHandler type '{handlerType}' has a method named '{name}' that does not match the required signature.");
}

return null;
}

// Remove this once there's a runtime API to subscribe to.
typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager")!.GetMethod("DeltaApplied", BindingFlags.Public | BindingFlags.Static)!.Invoke(null, null);
return (beforeUpdates, afterUpdates);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.Services.LazyAssemblyLoader.&lt;LoadAssembliesInClientAsync&gt;d__8.MoveNext</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<argument>IL2070</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.Services.LazyAssemblyLoader.&lt;LoadAssembliesInClientAsync&gt;d__8.MoveNext</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.&lt;GetMetadataUpdateHandlerActions&gt;g__GetUpdateMethod|7_0(System.Type,System.String)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand All @@ -49,11 +49,5 @@
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssemblyComponentParameterDeserializer.DeserializeParameters(System.Collections.Generic.IList{Microsoft.AspNetCore.Components.ComponentParameter},System.Collections.Generic.IList{System.Object})</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2075</argument>
<property name="Scope">member</property>
<property name="Target">M:Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])</property>
</attribute>
</assembly>
</linker>