Skip to content

Commit 04debec

Browse files
authored
Update the conventions for discovering and invoking metadata update handlers (#17460)
* Update the conventions for discovering and invoking metadata update handlers * Introduce ClearCache and UpdateApplication action names * Invoke actions in topologically sorted order.
1 parent 56a53ac commit 04debec

File tree

6 files changed

+349
-69
lines changed

6 files changed

+349
-69
lines changed

sdk.sln

+7
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.PackageVal
363363
EndProject
364364
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.PackageValidation.Tests", "src\Tests\Microsoft.DotNet.PackageValidation.Tests\Microsoft.DotNet.PackageValidation.Tests.csproj", "{69C03400-12AC-4E4D-B970-6A880616BF68}"
365365
EndProject
366+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.DotNetDeltaApplier.Tests", "src\Tests\Microsoft.Extensions.DotNetDeltaApplier.Tests\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "{FAAC2E23-A460-40FE-9207-C10EEE5A6A07}"
367+
EndProject
366368
Global
367369
GlobalSection(SolutionConfigurationPlatforms) = preSolution
368370
Debug|Any CPU = Debug|Any CPU
@@ -665,6 +667,10 @@ Global
665667
{69C03400-12AC-4E4D-B970-6A880616BF68}.Debug|Any CPU.Build.0 = Debug|Any CPU
666668
{69C03400-12AC-4E4D-B970-6A880616BF68}.Release|Any CPU.ActiveCfg = Release|Any CPU
667669
{69C03400-12AC-4E4D-B970-6A880616BF68}.Release|Any CPU.Build.0 = Release|Any CPU
670+
{FAAC2E23-A460-40FE-9207-C10EEE5A6A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
671+
{FAAC2E23-A460-40FE-9207-C10EEE5A6A07}.Debug|Any CPU.Build.0 = Debug|Any CPU
672+
{FAAC2E23-A460-40FE-9207-C10EEE5A6A07}.Release|Any CPU.ActiveCfg = Release|Any CPU
673+
{FAAC2E23-A460-40FE-9207-C10EEE5A6A07}.Release|Any CPU.Build.0 = Release|Any CPU
668674
EndGlobalSection
669675
GlobalSection(SolutionProperties) = preSolution
670676
HideSolutionNode = FALSE
@@ -786,6 +792,7 @@ Global
786792
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
787793
{E56BEA9A-B52A-4781-9FF4-217439923319} = {AF683E5C-421E-4DE0-ADD7-9841E5D12BFA}
788794
{69C03400-12AC-4E4D-B970-6A880616BF68} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
795+
{FAAC2E23-A460-40FE-9207-C10EEE5A6A07} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
789796
EndGlobalSection
790797
GlobalSection(ExtensibilityGlobals) = postSolution
791798
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}

src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs

+139-68
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
#nullable enable
5-
64
using System;
75
using System.Collections.Concurrent;
86
using System.Collections.Generic;
97
using System.Linq;
108
using System.Reflection;
11-
using System.Reflection.Metadata;
129
using Microsoft.DotNet.Watcher.Tools;
1310

1411
namespace Microsoft.Extensions.HotReload
@@ -19,7 +16,7 @@ internal class HotReloadAgent : IDisposable
1916
private readonly AssemblyLoadEventHandler _assemblyLoad;
2017
private readonly ConcurrentDictionary<Guid, IReadOnlyList<UpdateDelta>> _deltas = new();
2118
private readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
22-
private volatile UpdateHandlerActions? _beforeAfterUpdates;
19+
private volatile UpdateHandlerActions? _handlerActions;
2320

2421
public HotReloadAgent(Action<string> log)
2522
{
@@ -30,7 +27,7 @@ public HotReloadAgent(Action<string> log)
3027

3128
private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
3229
{
33-
_beforeAfterUpdates = null;
30+
_handlerActions = null;
3431
var loadedAssembly = eventArgs.LoadedAssembly;
3532
var moduleId = loadedAssembly.Modules.FirstOrDefault()?.ModuleVersionId;
3633
if (moduleId is null)
@@ -45,109 +42,183 @@ private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
4542
}
4643
}
4744

48-
private sealed class UpdateHandlerActions
45+
internal sealed class UpdateHandlerActions
4946
{
50-
public UpdateHandlerActions(List<Action<Type[]?>> before, List<Action<Type[]?>> after)
51-
{
52-
Before = before;
53-
After = after;
54-
}
55-
56-
public List<Action<Type[]?>> Before { get; }
57-
public List<Action<Type[]?>> After { get; }
47+
public List<Action<Type[]?>> Before { get; } = new();
48+
public List<Action<Type[]?>> After { get; } = new();
49+
public List<Action<Type[]?>> ClearCache { get; } = new();
50+
public List<Action<Type[]?>> UpdateApplication { get; } = new();
5851
}
5952

6053
private UpdateHandlerActions GetMetadataUpdateHandlerActions()
6154
{
62-
var before = new List<Action<Type[]?>>();
63-
var after = new List<Action<Type[]?>>();
64-
65-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
55+
// We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
56+
// sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
57+
// in System.Private.CoreLib is executed before System.Text.Json clears it's own cache.)
58+
// This would ensure that caches and updates more lower in the application stack are up to date
59+
// before ones higher in the stack are recomputed.
60+
var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
61+
var handlerActions = new UpdateHandlerActions();
62+
foreach (var assembly in sortedAssemblies)
6663
{
67-
foreach (var attr in assembly.GetCustomAttributes<MetadataUpdateHandlerAttribute>())
64+
foreach (var attr in assembly.GetCustomAttributesData())
6865
{
69-
bool methodFound = false;
70-
var handlerType = attr.HandlerType;
71-
72-
if (GetUpdateMethod(handlerType, "BeforeUpdate") is MethodInfo beforeUpdate)
66+
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
67+
// define their own copy without having to cross-compile.
68+
if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute")
7369
{
74-
before.Add(CreateAction(beforeUpdate));
75-
methodFound = true;
70+
continue;
7671
}
7772

78-
if (GetUpdateMethod(handlerType, "AfterUpdate") is MethodInfo afterUpdate)
73+
IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
74+
if (ctorArgs.Count != 1 ||
75+
ctorArgs[0].Value is not Type handlerType)
7976
{
80-
after.Add(CreateAction(afterUpdate));
81-
methodFound = true;
77+
_log($"'{attr}' found with invalid arguments.");
78+
continue;
8279
}
8380

84-
if (!methodFound)
81+
GetHandlerActions(handlerActions, handlerType);
82+
}
83+
}
84+
85+
return handlerActions;
86+
}
87+
88+
internal void GetHandlerActions(UpdateHandlerActions handlerActions, Type handlerType)
89+
{
90+
bool methodFound = false;
91+
92+
// Temporarily allow BeforeUpdate and AfterUpdate to be invoked until
93+
// everything is updated to use the new names.
94+
if (GetUpdateMethod(handlerType, "BeforeUpdate") is MethodInfo beforeUpdate)
95+
{
96+
handlerActions.Before.Add(CreateAction(beforeUpdate));
97+
methodFound = true;
98+
}
99+
100+
if (GetUpdateMethod(handlerType, "AfterUpdate") is MethodInfo afterUpdate)
101+
{
102+
handlerActions.After.Add(CreateAction(afterUpdate));
103+
methodFound = true;
104+
}
105+
106+
if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache)
107+
{
108+
handlerActions.ClearCache.Add(CreateAction(clearCache));
109+
methodFound = true;
110+
}
111+
112+
if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication)
113+
{
114+
handlerActions.UpdateApplication.Add(CreateAction(updateApplication));
115+
methodFound = true;
116+
}
117+
118+
if (!methodFound)
119+
{
120+
_log($"No invokable methods found on metadata handler type '{handlerType}'. " +
121+
$"Allowed methods are ClearCache, UpdateApplication");
122+
}
123+
124+
Action<Type[]?> CreateAction(MethodInfo update)
125+
{
126+
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
127+
return types =>
128+
{
129+
try
85130
{
86-
_log($"No BeforeUpdate or AfterUpdate method found on '{handlerType}'.");
131+
action(types);
87132
}
133+
catch (Exception ex)
134+
{
135+
_log($"Exception from '{action}': {ex}");
136+
}
137+
};
138+
}
139+
140+
MethodInfo? GetUpdateMethod(Type handlerType, string name)
141+
{
142+
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod &&
143+
updateMethod.ReturnType == typeof(void))
144+
{
145+
return updateMethod;
146+
}
88147

89-
Action<Type[]?> CreateAction(MethodInfo update)
148+
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
149+
{
150+
if (method.Name == name)
90151
{
91-
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
92-
return types =>
93-
{
94-
try
95-
{
96-
action(types);
97-
}
98-
catch (Exception ex)
99-
{
100-
_log($"Exception from '{action}': {ex}");
101-
}
102-
};
152+
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
153+
break;
103154
}
155+
}
156+
157+
return null;
158+
}
159+
}
160+
161+
internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
162+
{
163+
var sortedAssemblies = new List<Assembly>(assemblies.Length);
164+
165+
var visited = new HashSet<string>(StringComparer.Ordinal);
104166

105-
MethodInfo? GetUpdateMethod(Type handlerType, string name)
167+
foreach (var assembly in assemblies)
168+
{
169+
Visit(assemblies, assembly, sortedAssemblies, visited);
170+
}
171+
172+
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
173+
{
174+
var assemblyIdentifier = assembly.GetName().Name!;
175+
if (!visited.Add(assemblyIdentifier))
176+
{
177+
return;
178+
}
179+
180+
foreach (var dependencyName in assembly.GetReferencedAssemblies())
181+
{
182+
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
183+
if (dependency is not null)
106184
{
107-
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod &&
108-
updateMethod.ReturnType == typeof(void))
109-
{
110-
return updateMethod;
111-
}
112-
113-
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
114-
{
115-
if (method.Name == name)
116-
{
117-
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
118-
break;
119-
}
120-
}
121-
122-
return null;
185+
Visit(assemblies, dependency, sortedAssemblies, visited);
123186
}
124187
}
188+
189+
sortedAssemblies.Add(assembly);
125190
}
126191

127-
return new UpdateHandlerActions(before, after);
192+
return sortedAssemblies;
128193
}
129194

130195
public void ApplyDeltas(IReadOnlyList<UpdateDelta> deltas)
131196
{
132197
try
133198
{
134-
UpdateHandlerActions beforeAfterUpdates = _beforeAfterUpdates ??= GetMetadataUpdateHandlerActions();
199+
// Defer discovering the receiving deltas until the first hot reload delta.
200+
// This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
201+
_handlerActions ??= GetMetadataUpdateHandlerActions();
202+
var handlerActions = _handlerActions;
203+
204+
// TODO: Get types to pass in
205+
Type[]? updatedTypes = null;
135206

136-
beforeAfterUpdates.Before.ForEach(b => b(null)); // TODO: Get types to pass in
207+
handlerActions.Before.ForEach(b => b(updatedTypes));
137208

138-
foreach (var item in deltas)
209+
for (var i = 0; i < deltas.Count; i++)
139210
{
211+
var item = deltas[i];
140212
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == item.ModuleId);
141213
if (assembly is not null)
142214
{
143215
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
144216
}
145217
}
146218

147-
// Defer discovering the receiving deltas until the first hot reload delta.
148-
// This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
149-
150-
beforeAfterUpdates.After.ForEach(a => a(null)); // TODO: Get types to pass in
219+
handlerActions.ClearCache.ForEach(a => a(updatedTypes));
220+
handlerActions.After.ForEach(c => c(updatedTypes));
221+
handlerActions.UpdateApplication.ForEach(a => a(updatedTypes));
151222

152223
_log("Deltas applied.");
153224
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Runtime.CompilerServices;
5+
6+
[assembly: InternalsVisibleTo("Microsoft.Extensions.DotNetDeltaApplier.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

src/BuiltInTools/dotnet-watch/dotnet-watch.slnf

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
"solution": {
33
"path": "..\\..\\..\\sdk.sln",
44
"projects": [
5-
"src\\BuiltInTools\\DotNetDeltaApplier\\Microsoft.Extensions.DotNetDeltaApplier.csproj",
65
"src\\BuiltInTools\\BrowserRefresh\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj",
6+
"src\\BuiltInTools\\DotNetDeltaApplier\\Microsoft.Extensions.DotNetDeltaApplier.csproj",
77
"src\\BuiltInTools\\DotNetWatchTasks\\DotNetWatchTasks.csproj",
88
"src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj",
99
"src\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj",
1010
"src\\Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj",
11+
"src\\Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj",
1112
"src\\Tests\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj",
1213
"src\\Tests\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj"
1314
]

0 commit comments

Comments
 (0)