зеркало из https://github.com/dotnet/aspnetcore.git
Hot Reload agent improvements (#58333)
* Hot Reload agent improvements Refactor agent code so that it can be shared with dotnet-watch implementation via a shared project. Add new API applyHotReloadDeltas to apply updates. * Feedback and cleanup
This commit is contained in:
Родитель
be19faf14e
Коммит
0a5f4deafc
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -97,10 +97,15 @@ async function startCore(components: RootComponentManager<WebAssemblyComponentDe
|
|||
}
|
||||
});
|
||||
|
||||
// obsolete:
|
||||
Blazor._internal.applyHotReload = (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined, updatedTypes?: number[]) => {
|
||||
dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDelta', id, metadataDelta, ilDelta, pdbDelta, updatedTypes ?? null);
|
||||
};
|
||||
|
||||
Blazor._internal.applyHotReloadDeltas = (deltas: { moduleId: string, metadataDelta: string, ilDelta: string, pdbDelta: string, updatedTypes: number[] }[], loggingLevel: number) => {
|
||||
return dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDeltas', deltas, loggingLevel);
|
||||
};
|
||||
|
||||
Blazor._internal.getApplyUpdateCapabilities = () => dispatcher.invokeDotNetStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'GetApplyUpdateCapabilities');
|
||||
|
||||
// Configure JS interop
|
||||
|
|
|
@ -89,7 +89,11 @@ export interface IBlazor {
|
|||
}
|
||||
|
||||
// APIs invoked by hot reload
|
||||
|
||||
// obsolete:
|
||||
applyHotReload?: (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined, updatedTypes?: number[]) => void;
|
||||
|
||||
applyHotReloadDeltas?: (deltas: { moduleId: string, metadataDelta: string, ilDelta: string, pdbDelta: string, updatedTypes: number[] }[], loggingLevel: number) => {message: string, severity: number}[];
|
||||
getApplyUpdateCapabilities?: () => string;
|
||||
hotReloadApplied?: () => void;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
internal enum AgentMessageSeverity : byte
|
||||
{
|
||||
Verbose = 0,
|
||||
Warning = 1,
|
||||
Error = 2,
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
internal sealed class AgentReporter
|
||||
{
|
||||
private readonly List<(string message, AgentMessageSeverity severity)> _log = [];
|
||||
|
||||
public void Report(string message, AgentMessageSeverity severity)
|
||||
{
|
||||
_log.Add((message, severity));
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<(string message, AgentMessageSeverity severity)> GetAndClearLogEntries(ResponseLoggingLevel level)
|
||||
{
|
||||
lock (_log)
|
||||
{
|
||||
var filteredLog = (level != ResponseLoggingLevel.Verbose)
|
||||
? _log.Where(static entry => entry.severity != AgentMessageSeverity.Verbose)
|
||||
: _log;
|
||||
|
||||
var log = filteredLog.ToArray();
|
||||
_log.Clear();
|
||||
return log;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +1,105 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
// Based on the implementation in https://raw.githubusercontent.com/dotnet/sdk/aad0424c0bfaa60c8bd136a92fd131e53d14561a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace Microsoft.Extensions.HotReload;
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
#if NET
|
||||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
|
||||
#endif
|
||||
internal sealed class HotReloadAgent : IDisposable
|
||||
{
|
||||
/// Flags for hot reload handler Types like MVC's HotReloadService.
|
||||
private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
|
||||
private const string MetadataUpdaterTypeName = "System.Reflection.Metadata.MetadataUpdater";
|
||||
private const string ApplyUpdateMethodName = "ApplyUpdate";
|
||||
private const string GetCapabilitiesMethodName = "GetCapabilities";
|
||||
|
||||
private delegate void ApplyUpdateDelegate(Assembly assembly, ReadOnlySpan<byte> metadataDelta, ReadOnlySpan<byte> ilDelta, ReadOnlySpan<byte> pdbDelta);
|
||||
|
||||
private readonly Action<string> _log;
|
||||
private readonly AssemblyLoadEventHandler _assemblyLoad;
|
||||
private readonly ConcurrentDictionary<Guid, List<UpdateDelta>> _deltas = new();
|
||||
private readonly ConcurrentDictionary<Assembly, Assembly> _appliedAssemblies = new();
|
||||
private volatile UpdateHandlerActions? _handlerActions;
|
||||
private readonly ApplyUpdateDelegate _applyUpdate;
|
||||
private readonly MetadataUpdateHandlerInvoker _metadataUpdateHandlerInvoker;
|
||||
|
||||
public HotReloadAgent(Action<string> log)
|
||||
public AgentReporter Reporter { get; }
|
||||
public string Capabilities { get; }
|
||||
|
||||
private HotReloadAgent(AgentReporter reporter, ApplyUpdateDelegate applyUpdate, string capabilities)
|
||||
{
|
||||
_log = log;
|
||||
_assemblyLoad = OnAssemblyLoad;
|
||||
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad;
|
||||
Reporter = reporter;
|
||||
_metadataUpdateHandlerInvoker = new(reporter);
|
||||
_applyUpdate = applyUpdate;
|
||||
Capabilities = capabilities;
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
|
||||
}
|
||||
|
||||
public static bool TryCreate(AgentReporter reporter, [NotNullWhen(true)] out HotReloadAgent? agent)
|
||||
{
|
||||
GetUpdaterMethodsAndCapabilities(reporter, out var applyUpdate, out var capabilities);
|
||||
if (applyUpdate != null && !string.IsNullOrEmpty(capabilities))
|
||||
{
|
||||
agent = new HotReloadAgent(reporter, applyUpdate, capabilities);
|
||||
return true;
|
||||
}
|
||||
|
||||
agent = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
|
||||
}
|
||||
|
||||
private static void GetUpdaterMethodsAndCapabilities(AgentReporter reporter, out ApplyUpdateDelegate? applyUpdate, out string? capabilities)
|
||||
{
|
||||
applyUpdate = null;
|
||||
capabilities = null;
|
||||
|
||||
var metadataUpdater = Type.GetType(MetadataUpdaterTypeName + ", System.Runtime.Loader", throwOnError: false);
|
||||
if (metadataUpdater == null)
|
||||
{
|
||||
reporter.Report($"Type not found: {MetadataUpdaterTypeName}", AgentMessageSeverity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var applyUpdateMethod = metadataUpdater.GetMethod(ApplyUpdateMethodName, BindingFlags.Public | BindingFlags.Static, binder: null, [typeof(Assembly), typeof(ReadOnlySpan<byte>), typeof(ReadOnlySpan<byte>), typeof(ReadOnlySpan<byte>)], modifiers: null);
|
||||
if (applyUpdateMethod == null)
|
||||
{
|
||||
reporter.Report($"{MetadataUpdaterTypeName}.{ApplyUpdateMethodName} not found.", AgentMessageSeverity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
applyUpdate = (ApplyUpdateDelegate)applyUpdateMethod.CreateDelegate(typeof(ApplyUpdateDelegate));
|
||||
|
||||
var getCapabilities = metadataUpdater.GetMethod(GetCapabilitiesMethodName, BindingFlags.NonPublic | BindingFlags.Static, binder: null, Type.EmptyTypes, modifiers: null);
|
||||
if (getCapabilities == null)
|
||||
{
|
||||
reporter.Report($"{MetadataUpdaterTypeName}.{GetCapabilitiesMethodName} not found.", AgentMessageSeverity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
capabilities = getCapabilities.Invoke(obj: null, parameters: null) as string;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
reporter.Report($"Error retrieving capabilities: {e.Message}", AgentMessageSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
|
||||
{
|
||||
_handlerActions = null;
|
||||
_metadataUpdateHandlerInvoker.Clear();
|
||||
|
||||
var loadedAssembly = eventArgs.LoadedAssembly;
|
||||
var moduleId = TryGetModuleId(loadedAssembly);
|
||||
if (moduleId is null)
|
||||
|
@ -46,185 +114,29 @@ internal sealed class HotReloadAgent : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class UpdateHandlerActions
|
||||
public void ApplyDeltas(IEnumerable<UpdateDelta> deltas)
|
||||
{
|
||||
public List<Action<Type[]?>> ClearCache { get; } = new();
|
||||
public List<Action<Type[]?>> UpdateApplication { get; } = new();
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("Trimmer", "IL2072",
|
||||
Justification = "The handlerType passed to GetHandlerActions is preserved by MetadataUpdateHandlerAttribute with DynamicallyAccessedMemberTypes.All.")]
|
||||
private UpdateHandlerActions GetMetadataUpdateHandlerActions()
|
||||
{
|
||||
// We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
|
||||
// sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
|
||||
// in System.Private.CoreLib is executed before System.Text.Json clears it's own cache.)
|
||||
// This would ensure that caches and updates more lower in the application stack are up to date
|
||||
// before ones higher in the stack are recomputed.
|
||||
var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
|
||||
var handlerActions = new UpdateHandlerActions();
|
||||
foreach (var assembly in sortedAssemblies)
|
||||
foreach (var delta in deltas)
|
||||
{
|
||||
foreach (var attr in assembly.GetCustomAttributesData())
|
||||
{
|
||||
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
|
||||
// define their own copy without having to cross-compile.
|
||||
if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Reporter.Report($"Applying delta to module {delta.ModuleId}.", AgentMessageSeverity.Verbose);
|
||||
|
||||
IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
|
||||
if (ctorArgs.Count != 1 ||
|
||||
ctorArgs[0].Value is not Type handlerType)
|
||||
{
|
||||
_log($"'{attr}' found with invalid arguments.");
|
||||
continue;
|
||||
}
|
||||
|
||||
GetHandlerActions(handlerActions, handlerType);
|
||||
}
|
||||
}
|
||||
|
||||
return handlerActions;
|
||||
}
|
||||
|
||||
internal void GetHandlerActions(
|
||||
UpdateHandlerActions handlerActions,
|
||||
[DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType)
|
||||
{
|
||||
bool methodFound = false;
|
||||
|
||||
if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache)
|
||||
{
|
||||
handlerActions.ClearCache.Add(CreateAction(clearCache));
|
||||
methodFound = true;
|
||||
}
|
||||
|
||||
if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication)
|
||||
{
|
||||
handlerActions.UpdateApplication.Add(CreateAction(updateApplication));
|
||||
methodFound = true;
|
||||
}
|
||||
|
||||
if (!methodFound)
|
||||
{
|
||||
_log($"No invokable methods found on metadata handler type '{handlerType}'. " +
|
||||
$"Allowed methods are ClearCache, UpdateApplication");
|
||||
}
|
||||
|
||||
Action<Type[]?> CreateAction(MethodInfo update)
|
||||
{
|
||||
Action<Type[]?> action = update.CreateDelegate<Action<Type[]?>>();
|
||||
return types =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action(types);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"Exception from '{action}': {ex}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MethodInfo? GetUpdateMethod([DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType, string name)
|
||||
{
|
||||
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, new[] { typeof(Type[]) }) is MethodInfo updateMethod &&
|
||||
updateMethod.ReturnType == typeof(void))
|
||||
{
|
||||
return updateMethod;
|
||||
}
|
||||
|
||||
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (method.Name == name)
|
||||
{
|
||||
_log($"Type '{handlerType}' has method '{method}' that does not match the required signature.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
|
||||
{
|
||||
var sortedAssemblies = new List<Assembly>(assemblies.Length);
|
||||
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Visit(assemblies, assembly, sortedAssemblies, visited);
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
|
||||
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
|
||||
{
|
||||
var assemblyIdentifier = assembly.GetName().Name!;
|
||||
if (!visited.Add(assemblyIdentifier))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dependencyName in assembly.GetReferencedAssemblies())
|
||||
{
|
||||
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
|
||||
if (dependency is not null)
|
||||
{
|
||||
Visit(assemblies, dependency, sortedAssemblies, visited);
|
||||
}
|
||||
}
|
||||
|
||||
sortedAssemblies.Add(assembly);
|
||||
}
|
||||
|
||||
return sortedAssemblies;
|
||||
}
|
||||
|
||||
public void ApplyDeltas(IReadOnlyList<UpdateDelta> deltas)
|
||||
{
|
||||
for (var i = 0; i < deltas.Count; i++)
|
||||
{
|
||||
var item = deltas[i];
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (TryGetModuleId(assembly) is Guid moduleId && moduleId == item.ModuleId)
|
||||
if (TryGetModuleId(assembly) is Guid moduleId && moduleId == delta.ModuleId)
|
||||
{
|
||||
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, item.PdbBytes ?? ReadOnlySpan<byte>.Empty);
|
||||
_applyUpdate(assembly, delta.MetadataDelta, delta.ILDelta, delta.PdbDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally stash the deltas away so it may be applied to assemblies loaded later.
|
||||
var cachedDeltas = _deltas.GetOrAdd(item.ModuleId, static _ => new());
|
||||
cachedDeltas.Add(item);
|
||||
var cachedDeltas = _deltas.GetOrAdd(delta.ModuleId, static _ => new());
|
||||
cachedDeltas.Add(delta);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Defer discovering metadata updata handlers until after hot reload deltas have been applied.
|
||||
// This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
|
||||
_handlerActions ??= GetMetadataUpdateHandlerActions();
|
||||
var handlerActions = _handlerActions;
|
||||
|
||||
Type[]? updatedTypes = GetMetadataUpdateTypes(deltas);
|
||||
|
||||
handlerActions.ClearCache.ForEach(a => a(updatedTypes));
|
||||
handlerActions.UpdateApplication.ForEach(a => a(updatedTypes));
|
||||
|
||||
_log("Deltas applied.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log(ex.ToString());
|
||||
}
|
||||
_metadataUpdateHandlerInvoker.Invoke(GetMetadataUpdateTypes(deltas));
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
|
||||
private static Type[] GetMetadataUpdateTypes(IReadOnlyList<UpdateDelta> deltas)
|
||||
private Type[] GetMetadataUpdateTypes(IEnumerable<UpdateDelta> deltas)
|
||||
{
|
||||
List<Type>? types = null;
|
||||
|
||||
|
@ -236,44 +148,45 @@ internal sealed class HotReloadAgent : IDisposable
|
|||
continue;
|
||||
}
|
||||
|
||||
var assemblyTypes = assembly.GetTypes();
|
||||
|
||||
foreach (var updatedType in delta.UpdatedTypes ?? Array.Empty<int>())
|
||||
foreach (var updatedType in delta.UpdatedTypes)
|
||||
{
|
||||
var type = assemblyTypes.FirstOrDefault(t => t.MetadataToken == updatedType);
|
||||
if (type != null)
|
||||
// Must be a TypeDef.
|
||||
Debug.Assert(MetadataTokens.EntityHandle(updatedType) is { Kind: HandleKind.TypeDefinition, IsNil: false });
|
||||
|
||||
// The type has to be in the manifest module since Hot Reload does not support multi-module assemblies:
|
||||
try
|
||||
{
|
||||
var type = assembly.ManifestModule.ResolveType(updatedType);
|
||||
types ??= new();
|
||||
types.Add(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Reporter.Report($"Failed to load type 0x{updatedType:X8}: {e.Message}", AgentMessageSeverity.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types?.ToArray() ?? Type.EmptyTypes;
|
||||
}
|
||||
|
||||
public void ApplyDeltas(Assembly assembly, IReadOnlyList<UpdateDelta> deltas)
|
||||
private void ApplyDeltas(Assembly assembly, IReadOnlyList<UpdateDelta> deltas)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var item in deltas)
|
||||
{
|
||||
MetadataUpdater.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan<byte>.Empty);
|
||||
_applyUpdate(assembly, item.MetadataDelta, item.ILDelta, item.PdbDelta);
|
||||
}
|
||||
|
||||
_log("Deltas applied.");
|
||||
Reporter.Report("Deltas applied.", AgentMessageSeverity.Verbose);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log(ex.ToString());
|
||||
Reporter.Report(ex.ToString(), AgentMessageSeverity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoad;
|
||||
}
|
||||
|
||||
private static Guid? TryGetModuleId(Assembly loadedAssembly)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
/// <summary>
|
||||
/// Finds and invokes metadata update handlers.
|
||||
/// </summary>
|
||||
#if NET
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot reload is only expected to work when trimming is disabled.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Hot reload is only expected to work when trimming is disabled.")]
|
||||
#endif
|
||||
internal sealed class MetadataUpdateHandlerInvoker(AgentReporter reporter)
|
||||
{
|
||||
internal sealed class RegisteredActions(IReadOnlyList<Action<Type[]?>> clearCache, IReadOnlyList<Action<Type[]?>> updateApplication)
|
||||
{
|
||||
public void Invoke(Type[] updatedTypes)
|
||||
{
|
||||
foreach (var action in clearCache)
|
||||
{
|
||||
action(updatedTypes);
|
||||
}
|
||||
|
||||
foreach (var action in updateApplication)
|
||||
{
|
||||
action(updatedTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For testing.
|
||||
/// </summary>
|
||||
internal IEnumerable<Action<Type[]?>> ClearCache => clearCache;
|
||||
|
||||
/// <summary>
|
||||
/// For testing.
|
||||
/// </summary>
|
||||
internal IEnumerable<Action<Type[]?>> UpdateApplication => updateApplication;
|
||||
}
|
||||
|
||||
private const string ClearCacheHandlerName = "ClearCache";
|
||||
private const string UpdateApplicationHandlerName = "UpdateApplication";
|
||||
|
||||
private RegisteredActions? _actions;
|
||||
|
||||
/// <summary>
|
||||
/// Call when a new assembly is loaded.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
=> Interlocked.Exchange(ref _actions, null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes all registerd handlers.
|
||||
/// </summary>
|
||||
internal void Invoke(Type[] updatedTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Defer discovering metadata updata handlers until after hot reload deltas have been applied.
|
||||
// This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
|
||||
var actions = _actions;
|
||||
if (actions == null)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _actions, GetMetadataUpdateHandlerActions(), null);
|
||||
actions = _actions;
|
||||
}
|
||||
|
||||
reporter.Report($"Invoking metadata update handlers. {updatedTypes.Length} type(s) updated.", AgentMessageSeverity.Verbose);
|
||||
|
||||
actions.Invoke(updatedTypes);
|
||||
|
||||
reporter.Report("Deltas applied.", AgentMessageSeverity.Verbose);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
reporter.Report(e.ToString(), AgentMessageSeverity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetHandlerTypes()
|
||||
{
|
||||
// We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
|
||||
// sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
|
||||
// in System.Private.CoreLib is executed before System.Text.Json clears its own cache.)
|
||||
// This would ensure that caches and updates more lower in the application stack are up to date
|
||||
// before ones higher in the stack are recomputed.
|
||||
var sortedAssemblies = TopologicalSort(AppDomain.CurrentDomain.GetAssemblies());
|
||||
|
||||
foreach (var assembly in sortedAssemblies)
|
||||
{
|
||||
foreach (var attr in TryGetCustomAttributesData(assembly))
|
||||
{
|
||||
// Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
|
||||
// define their own copy without having to cross-compile.
|
||||
if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IList<CustomAttributeTypedArgument> ctorArgs = attr.ConstructorArguments;
|
||||
if (ctorArgs.Count != 1 ||
|
||||
ctorArgs[0].Value is not Type handlerType)
|
||||
{
|
||||
reporter.Report($"'{attr}' found with invalid arguments.", AgentMessageSeverity.Warning);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return handlerType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RegisteredActions GetMetadataUpdateHandlerActions()
|
||||
=> GetMetadataUpdateHandlerActions(GetHandlerTypes());
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing.
|
||||
/// </summary>
|
||||
internal RegisteredActions GetMetadataUpdateHandlerActions(IEnumerable<Type> handlerTypes)
|
||||
{
|
||||
var clearCacheActions = new List<Action<Type[]?>>();
|
||||
var updateApplicationActions = new List<Action<Type[]?>>();
|
||||
|
||||
foreach (var handlerType in handlerTypes)
|
||||
{
|
||||
bool methodFound = false;
|
||||
|
||||
if (GetUpdateMethod(handlerType, ClearCacheHandlerName) is MethodInfo clearCache)
|
||||
{
|
||||
clearCacheActions.Add(CreateAction(clearCache));
|
||||
methodFound = true;
|
||||
}
|
||||
|
||||
if (GetUpdateMethod(handlerType, UpdateApplicationHandlerName) is MethodInfo updateApplication)
|
||||
{
|
||||
updateApplicationActions.Add(CreateAction(updateApplication));
|
||||
methodFound = true;
|
||||
}
|
||||
|
||||
if (!methodFound)
|
||||
{
|
||||
reporter.Report(
|
||||
$"Expected to find a static method '{ClearCacheHandlerName}' or '{UpdateApplicationHandlerName}' on type '{handlerType.AssemblyQualifiedName}' but neither exists.",
|
||||
AgentMessageSeverity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
return new RegisteredActions(clearCacheActions, updateApplicationActions);
|
||||
|
||||
Action<Type[]?> CreateAction(MethodInfo update)
|
||||
{
|
||||
var action = (Action<Type[]?>)update.CreateDelegate(typeof(Action<Type[]?>));
|
||||
return types =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action(types);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reporter.Report($"Exception from '{action}': {ex}", AgentMessageSeverity.Warning);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MethodInfo? GetUpdateMethod(Type handlerType, string name)
|
||||
{
|
||||
if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, binder: null, [typeof(Type[])], modifiers: null) is MethodInfo updateMethod &&
|
||||
updateMethod.ReturnType == typeof(void))
|
||||
{
|
||||
return updateMethod;
|
||||
}
|
||||
|
||||
foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
|
||||
{
|
||||
if (method.Name == name)
|
||||
{
|
||||
reporter.Report($"Type '{handlerType}' has method '{method}' that does not match the required signature.", AgentMessageSeverity.Warning);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IList<CustomAttributeData> TryGetCustomAttributesData(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetCustomAttributesData();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// In cross-platform scenarios, such as debugging in VS through WSL, Roslyn
|
||||
// runs on Windows, and the agent runs on Linux. Assemblies accessible to Windows
|
||||
// may not be available or loaded on linux (such as WPF's assemblies).
|
||||
// In such case, we can ignore the assemblies and continue enumerating handlers for
|
||||
// the rest of the assemblies of current domain.
|
||||
reporter.Report($"'{assembly.FullName}' is not loaded ({e.Message})", AgentMessageSeverity.Verbose);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing.
|
||||
/// </summary>
|
||||
internal static List<Assembly> TopologicalSort(Assembly[] assemblies)
|
||||
{
|
||||
var sortedAssemblies = new List<Assembly>(assemblies.Length);
|
||||
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Visit(assemblies, assembly, sortedAssemblies, visited);
|
||||
}
|
||||
|
||||
static void Visit(Assembly[] assemblies, Assembly assembly, List<Assembly> sortedAssemblies, HashSet<string> visited)
|
||||
{
|
||||
var assemblyIdentifier = assembly.GetName().Name!;
|
||||
if (!visited.Add(assemblyIdentifier))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dependencyName in assembly.GetReferencedAssemblies())
|
||||
{
|
||||
var dependency = Array.Find(assemblies, a => a.GetName().Name == dependencyName.Name);
|
||||
if (dependency is not null)
|
||||
{
|
||||
Visit(assemblies, dependency, sortedAssemblies, visited);
|
||||
}
|
||||
}
|
||||
|
||||
sortedAssemblies.Add(assembly);
|
||||
}
|
||||
|
||||
return sortedAssemblies;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
internal enum ResponseLoggingLevel : byte
|
||||
{
|
||||
WarningsAndErrors = 0,
|
||||
Verbose = 1,
|
||||
}
|
|
@ -1,17 +1,13 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.Extensions.HotReload;
|
||||
namespace Microsoft.DotNet.HotReload;
|
||||
|
||||
internal sealed class UpdateDelta
|
||||
internal readonly struct UpdateDelta(Guid moduleId, byte[] metadataDelta, byte[] ilDelta, byte[] pdbDelta, int[] updatedTypes)
|
||||
{
|
||||
public Guid ModuleId { get; set; }
|
||||
|
||||
public byte[] MetadataDelta { get; set; } = default!;
|
||||
|
||||
public byte[] ILDelta { get; set; } = default!;
|
||||
|
||||
public byte[]? PdbBytes { get; set; }
|
||||
|
||||
public int[]? UpdatedTypes { get; set; }
|
||||
public Guid ModuleId { get; } = moduleId;
|
||||
public byte[] MetadataDelta { get; } = metadataDelta;
|
||||
public byte[] ILDelta { get; } = ilDelta;
|
||||
public byte[] PdbDelta { get; } = pdbDelta;
|
||||
public int[] UpdatedTypes { get; } = updatedTypes;
|
||||
}
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using Microsoft.Extensions.HotReload;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.DotNet.HotReload;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
|
||||
|
||||
/// <summary>
|
||||
|
@ -16,49 +20,143 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
|
|||
/// code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[UnconditionalSuppressMessage(
|
||||
"Trimming",
|
||||
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
|
||||
Justification = "Hot Reload does not support trimming")]
|
||||
public static partial class WebAssemblyHotReload
|
||||
{
|
||||
private const string BlazorHotReloadModuleName = "blazor-hotreload";
|
||||
|
||||
private static HotReloadAgent? _hotReloadAgent;
|
||||
private static readonly UpdateDelta[] _updateDeltas = new[]
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
public readonly struct LogEntry
|
||||
{
|
||||
new UpdateDelta(),
|
||||
};
|
||||
public string Message { get; init; }
|
||||
public int Severity { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
internal sealed class Update
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Delta[] Deltas { get; set; } = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
public readonly struct Delta
|
||||
{
|
||||
public string ModuleId { get; init; }
|
||||
public byte[] MetadataDelta { get; init; }
|
||||
public byte[] ILDelta { get; init; }
|
||||
public byte[] PdbDelta { get; init; }
|
||||
public int[] UpdatedTypes { get; init; }
|
||||
}
|
||||
|
||||
private static readonly AgentReporter s_reporter = new();
|
||||
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private static bool s_initialized;
|
||||
private static HotReloadAgent? s_hotReloadAgent;
|
||||
|
||||
internal static async Task InitializeAsync()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("__ASPNETCORE_BROWSER_TOOLS") == "true" &&
|
||||
OperatingSystem.IsBrowser())
|
||||
{
|
||||
// Attempt to read previously applied hot reload deltas if the ASP.NET Core browser tools are available (indicated by the presence of the Environment variable).
|
||||
// The agent is injected in to the hosted app and can serve this script that can provide results from local-storage.
|
||||
// See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
|
||||
await JSHost.ImportAsync(BlazorHotReloadModuleName, "/_framework/blazor-hotreload.js");
|
||||
await ReceiveHotReloadAsync();
|
||||
s_initialized = true;
|
||||
|
||||
if (!HotReloadAgent.TryCreate(s_reporter, out var agent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingAgent = Interlocked.CompareExchange(ref s_hotReloadAgent, agent, null);
|
||||
if (existingAgent != null)
|
||||
{
|
||||
throw new InvalidOperationException("Hot Reload agent already initialized");
|
||||
}
|
||||
|
||||
await ApplyPreviousDeltasAsync(agent);
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask ApplyPreviousDeltasAsync(HotReloadAgent agent)
|
||||
{
|
||||
string errorMessage;
|
||||
|
||||
using var client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(WebAssemblyNavigationManager.Instance.BaseUri, UriKind.Absolute)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync("/_framework/blazor-hotreload");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var deltasJson = await response.Content.ReadAsStringAsync();
|
||||
var updates = deltasJson != "" ? JsonSerializer.Deserialize<Update[]>(deltasJson, s_jsonSerializerOptions) : null;
|
||||
if (updates == null)
|
||||
{
|
||||
s_reporter.Report($"No previous updates to apply.", AgentMessageSeverity.Verbose);
|
||||
return;
|
||||
}
|
||||
|
||||
var i = 1;
|
||||
foreach (var update in updates)
|
||||
{
|
||||
s_reporter.Report($"Reapplying update {i}/{updates.Length}.", AgentMessageSeverity.Verbose);
|
||||
|
||||
agent.ApplyDeltas(
|
||||
update.Deltas.Select(d => new UpdateDelta(Guid.Parse(d.ModuleId, CultureInfo.InvariantCulture), d.MetadataDelta, d.ILDelta, d.PdbDelta, d.UpdatedTypes)));
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
errorMessage = $"HTTP GET '/_framework/blazor-hotreload' returned {response.StatusCode}";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
errorMessage = e.ToString();
|
||||
}
|
||||
|
||||
s_reporter.Report($"Failed to retrieve and apply previous deltas from the server: ${errorMessage}", AgentMessageSeverity.Error);
|
||||
}
|
||||
|
||||
private static HotReloadAgent? GetAgent()
|
||||
=> s_hotReloadAgent ?? (s_initialized ? throw new InvalidOperationException("Hot Reload agent not initialized") : null);
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
[Obsolete("Use ApplyHotReloadDeltas instead")]
|
||||
[JSInvokable(nameof(ApplyHotReloadDelta))]
|
||||
public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDelta, byte[] ilDelta, byte[] pdbBytes, int[]? updatedTypes)
|
||||
{
|
||||
GetAgent()?.ApplyDeltas(
|
||||
[new UpdateDelta(Guid.Parse(moduleIdString, CultureInfo.InvariantCulture), metadataDelta, ilDelta, pdbBytes, updatedTypes ?? [])]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
/// </summary>
|
||||
[JSInvokable(nameof(ApplyHotReloadDelta))]
|
||||
public static void ApplyHotReloadDelta(string moduleIdString, byte[] metadataDelta, byte[] ilDelta, byte[] pdbBytes, int[]? updatedTypes)
|
||||
[JSInvokable(nameof(ApplyHotReloadDeltas))]
|
||||
public static LogEntry[] ApplyHotReloadDeltas(Delta[] deltas, int loggingLevel)
|
||||
{
|
||||
// Analyzer has a bug where it doesn't handle ConditionalAttribute: https://github.com/dotnet/roslyn/issues/63464
|
||||
#pragma warning disable IDE0200 // Remove unnecessary lambda expression
|
||||
Interlocked.CompareExchange(ref _hotReloadAgent, new HotReloadAgent(m => Debug.WriteLine(m)), null);
|
||||
#pragma warning restore IDE0200 // Remove unnecessary lambda expression
|
||||
var agent = GetAgent();
|
||||
|
||||
var moduleId = Guid.Parse(moduleIdString, CultureInfo.InvariantCulture);
|
||||
agent?.ApplyDeltas(
|
||||
deltas.Select(d => new UpdateDelta(Guid.Parse(d.ModuleId, CultureInfo.InvariantCulture), d.MetadataDelta, d.ILDelta, d.PdbDelta, d.UpdatedTypes)));
|
||||
|
||||
_updateDeltas[0].ModuleId = moduleId;
|
||||
_updateDeltas[0].MetadataDelta = metadataDelta;
|
||||
_updateDeltas[0].ILDelta = ilDelta;
|
||||
_updateDeltas[0].PdbBytes = pdbBytes;
|
||||
_updateDeltas[0].UpdatedTypes = updatedTypes;
|
||||
|
||||
_hotReloadAgent.ApplyDeltas(_updateDeltas);
|
||||
return s_reporter.GetAndClearLogEntries((ResponseLoggingLevel)loggingLevel)
|
||||
.Select(log => new LogEntry() { Message = log.message, Severity = (int)log.severity }).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -66,15 +164,5 @@ public static partial class WebAssemblyHotReload
|
|||
/// </summary>
|
||||
[JSInvokable(nameof(GetApplyUpdateCapabilities))]
|
||||
public static string GetApplyUpdateCapabilities()
|
||||
{
|
||||
var method = typeof(System.Reflection.Metadata.MetadataUpdater).GetMethod("GetCapabilities", BindingFlags.NonPublic | BindingFlags.Static, Type.EmptyTypes);
|
||||
if (method is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return (string)method.Invoke(obj: null, parameters: null)!;
|
||||
}
|
||||
|
||||
[JSImport("receiveHotReloadAsync", BlazorHotReloadModuleName)]
|
||||
private static partial Task ReceiveHotReloadAsync();
|
||||
=> GetAgent()?.Capabilities ?? "";
|
||||
}
|
||||
|
|
|
@ -81,6 +81,24 @@ Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostConfiguration
|
|||
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostConfiguration.WebAssemblyHostConfiguration() -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.Delta() -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.get -> byte[]!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ILDelta.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.MetadataDelta.get -> byte[]!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.MetadataDelta.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ModuleId.get -> string!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.ModuleId.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.PdbDelta.get -> byte[]!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.PdbDelta.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.UpdatedTypes.get -> int[]!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta.UpdatedTypes.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.LogEntry() -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.get -> string!
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Message.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.get -> int
|
||||
Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry.Severity.init -> void
|
||||
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
|
||||
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache.Default = 0 -> Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
|
||||
Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache.ForceCache = 4 -> Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache
|
||||
|
@ -108,6 +126,7 @@ static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnviro
|
|||
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions.IsProduction(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment! hostingEnvironment) -> bool
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostEnvironmentExtensions.IsStaging(this Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment! hostingEnvironment) -> bool
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDelta(string! moduleIdString, byte[]! metadataDelta, byte[]! ilDelta, byte[]! pdbBytes, int[]? updatedTypes) -> void
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.ApplyHotReloadDeltas(Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.Delta[]! deltas, int loggingLevel) -> Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.LogEntry[]!
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.HotReload.WebAssemblyHotReload.GetApplyUpdateCapabilities() -> string!
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestCache(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCache requestCache) -> System.Net.Http.HttpRequestMessage!
|
||||
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestCredentials(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestCredentials requestCredentials) -> System.Net.Http.HttpRequestMessage!
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.Extensions.HotReload;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
|
||||
|
||||
public class WebAssemblyHotReloadTest
|
||||
{
|
||||
[Fact]
|
||||
public void WebAssemblyHotReload_DiscoversMetadataHandlers_FromHot()
|
||||
{
|
||||
// Arrange
|
||||
var hotReloadManager = typeof(Renderer).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager");
|
||||
Assert.NotNull(hotReloadManager);
|
||||
|
||||
var handlerActions = new HotReloadAgent.UpdateHandlerActions();
|
||||
var logs = new List<string>();
|
||||
var hotReloadAgent = new HotReloadAgent(logs.Add);
|
||||
|
||||
// Act
|
||||
hotReloadAgent.GetHandlerActions(handlerActions, hotReloadManager);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(handlerActions.ClearCache);
|
||||
Assert.Single(handlerActions.UpdateApplication);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче