diff --git a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs index c7b325a4c348..3cacff6b82c2 100644 --- a/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/DefaultHotReloadClient.cs @@ -77,12 +77,15 @@ async Task> ConnectAsync() // When the client connects, the first payload it sends is the initialization payload which includes the apply capabilities. var capabilities = (await ClientInitializationResponse.ReadAsync(_pipe, cancellationToken)).Capabilities; - Logger.Log(LogEvents.Capabilities, capabilities); + + var result = AddImplicitCapabilities(capabilities.Split(' ')); + + Logger.Log(LogEvents.Capabilities, string.Join(" ", result)); // fire and forget: _ = ListenForResponsesAsync(cancellationToken); - return [.. capabilities.Split(' ')]; + return result; } catch (Exception e) when (e is not OperationCanceledException) { diff --git a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs index 2a563419e4f2..e8bccc3ee90e 100644 --- a/src/BuiltInTools/HotReloadClient/HotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/HotReloadClient.cs @@ -41,6 +41,13 @@ internal abstract class HotReloadClient(ILogger logger, ILogger agentLogger) : I internal Task PendingUpdates => _pendingUpdates; + /// + /// .NET Framework runtime does not support adding MethodImpl entries, therefore the capability is not in the baseline capability set. + /// All other runtimes (.NET and Mono) support it and rather than servicing all of them we include the capability here. + /// + protected static ImmutableArray AddImplicitCapabilities(IEnumerable capabilities) + => [.. capabilities, "AddExplicitInterfaceImplementation"]; + public abstract void ConfigureLaunchEnvironment(IDictionary environmentBuilder); /// diff --git a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs index f47d6eb3a5ba..d4a165e0793a 100644 --- a/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs +++ b/src/BuiltInTools/HotReloadClient/Web/WebAssemblyHotReloadClient.cs @@ -41,24 +41,30 @@ internal sealed class WebAssemblyHotReloadClient( private static ImmutableArray GetUpdateCapabilities(ILogger logger, ImmutableArray projectHotReloadCapabilities, Version projectTargetFrameworkVersion) { - var capabilities = projectHotReloadCapabilities; - - if (capabilities.IsEmpty) - { - logger.LogDebug("Using capabilities based on project target framework version: '{Version}'.", projectTargetFrameworkVersion); - - capabilities = projectTargetFrameworkVersion.Major switch + var capabilities = projectHotReloadCapabilities.IsEmpty + ? projectTargetFrameworkVersion.Major switch { 9 => s_defaultCapabilities90, 8 => s_defaultCapabilities80, 7 => s_defaultCapabilities70, 6 => s_defaultCapabilities60, _ => [], - }; + } + : projectHotReloadCapabilities; + + if (capabilities is not []) + { + capabilities = AddImplicitCapabilities(capabilities); + } + + var capabilitiesStr = string.Join(", ", capabilities); + if (projectHotReloadCapabilities.IsEmpty) + { + logger.LogDebug("Project specifies capabilities: {Capabilities}.", capabilitiesStr); } else { - logger.LogDebug("Project specifies capabilities: '{Capabilities}'", string.Join(" ", capabilities)); + logger.LogDebug("Using capabilities based on project target framework version: '{Version}': {Capabilities}.", projectTargetFrameworkVersion, capabilitiesStr); } return capabilities; diff --git a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs index 1031aa39be35..a58c43f7dcfe 100644 --- a/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs +++ b/test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs @@ -57,7 +57,7 @@ public async Task ApplyManagedCodeUpdates_ProcessNotSuspended() await using var test = new Test(output, agent); var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None); - AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities); + AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "AddExplicitInterfaceImplementation"], actualCapabilities); var update = new HotReloadManagedCodeUpdate( moduleId: moduleId, @@ -86,7 +86,7 @@ public async Task ApplyManagedCodeUpdates_ProcessSuspended() await using var test = new Test(output, agent); var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None); - AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities); + AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "AddExplicitInterfaceImplementation"], actualCapabilities); var update = new HotReloadManagedCodeUpdate( moduleId: moduleId, @@ -125,7 +125,7 @@ public async Task ApplyManagedCodeUpdates_Failure() await using var test = new Test(output, agent); var actualCapabilities = await test.Client.GetUpdateCapabilitiesAsync(CancellationToken.None); - AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType"], actualCapabilities); + AssertEx.SequenceEqual(["Baseline", "AddMethodToExistingType", "AddStaticFieldToExistingType", "AddExplicitInterfaceImplementation"], actualCapabilities); var update = new HotReloadManagedCodeUpdate( moduleId: Guid.NewGuid(), diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index 833b2bc5a6c8..5b0e6c9a2057 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -69,6 +69,8 @@ public static void Print() UpdateSourceFile(Path.Combine(dependencyDir, "Foo.cs"), newSrc); await App.AssertOutputLineStartsWith("Changed!"); + + App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); } [Fact] @@ -892,11 +894,11 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) // check project specified capapabilities: if (projectSpecifiesCapabilities) { - App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType."); + App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddMethodToExistingType Baseline."); } else { - App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType."); + App.AssertOutputContains("dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters."); } } @@ -963,10 +965,10 @@ public async Task BlazorWasmHosted() App.AssertOutputContains(MessageDescriptor.ApplicationKind_BlazorHosted); // client capabilities: - App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Project 'blazorwasm ({tfm})' specifies capabilities: 'Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType'"); + App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Project specifies capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType NewTypeDefinition ChangeCustomAttributes AddInstanceFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod UpdateParameters GenericAddFieldToExistingType AddExplicitInterfaceImplementation."); // server capabilities: - App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Capabilities: 'Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva'"); + App.AssertOutputContains($"dotnet watch ⌚ [blazorhosted ({tfm})] Capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva AddExplicitInterfaceImplementation."); } [PlatformSpecificFact(TestPlatforms.Windows)] // https://github.com/dotnet/aspnetcore/issues/63759