Annotate SignalR server for native AOT#56460
Conversation
Fix SignalR Server's usage of MakeGenericMethod when using a streaming reader (IAsyncEnumerable or ChannelReader) following the same approach as the client. Add a runtime check and throw an exception when trying to stream a ValueType in native AOT. Adjust the public annotations: * Remove RequiresUnreferencedCode from AddSignalR * Add RequiresUnreferencedCode to MessagePack and NewtonsoftJson protocols Support trimming and AOT in DefaultHubDispatcher by adding a feature switch to turn off custom awaitable support. Update ObjectMethodExecutor to support trimming and AOT by a new method that doesn't look for custom awaitables, and uses reflection instead of Linq.Expressions.
| } | ||
|
|
||
| private static IEnumerable<MethodInfo> GetAllInterfaceMethods(Type interfaceType) | ||
| [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", |
There was a problem hiding this comment.
Note the suppressions in this file are the same as in https://github.com/dotnet/runtime/blob/3ec76324514868b0ee4cfcca43ef96a4d62d44fa/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs#L168-L174
* Move RequiresDynamicCode to the whole Hub<T> class, so developers always get a warning when using IHubContext<THub, T>. This helps because THub needs to be a Hub<T>, which will warn as soon as it is used. * Suppress the warning in AddSignalRCore that references HubContext<THub, T> since users will get warnings when they try using IHubContext<THub, T>.
| private readonly Func<HubLifetimeContext, Exception?, Task>? _onDisconnectedMiddleware; | ||
| private readonly HubLifetimeManager<THub> _hubLifetimeManager; | ||
|
|
||
| [FeatureSwitchDefinition("Microsoft.AspNetCore.SignalR.Hub.CustomAwaitableSupport")] |
There was a problem hiding this comment.
Could probably remove Hub from the name:
Microsoft.AspNetCore.SignalR.CustomAwaitableSupport
There was a problem hiding this comment.
My recommendation on feature switches is to name them in such a way in case we ever need to make them as a public API, like we did with JsonSerializer.IsReflectionEnabledByDefault. So that's the pattern I've been following lately.
See also:
There was a problem hiding this comment.
FYI - I'm renaming this switch to Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported to align with the naming of other feature switches:
- System.Diagnostics.Tracing.EventSource.IsSupported
- System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported
- System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault
- System.ComponentModel.TypeDescriptor.IsComObjectDescriptorSupported
I just don't want to reset CI right now, so will push that change after CI is done running.
This is now done.
| RunNativeAotTest(static async () => | ||
| { | ||
| //System.Diagnostics.Debugger.Launch(); | ||
| var loggerFactory = new StringLoggerFactory(); |
There was a problem hiding this comment.
Can we remove this? If you call StartServer it already configures a logger factory that is wired up to file logging (and VS logging).
There was a problem hiding this comment.
StartServer requires you to pass an ILoggerFactory in:
Also, we need to assert the logs contains the right string for the tests that return just Task/ValueTask to ensure they got called correctly.
There was a problem hiding this comment.
There was a problem hiding this comment.
The problem with that is that this code is running in a new process (spun up by the RemoteExecutor) and so we don't have access to the instance of the Test. And things like TestSink and ITestOutputHelper aren't available in the separate process.
| /// A context abstraction for a hub. | ||
| /// </summary> | ||
| public interface IHubContext<THub, T> | ||
| public interface IHubContext<THub, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> |
There was a problem hiding this comment.
Did we want to put RequiresDynamicCode on this type like we did for the internal implementation?
There was a problem hiding this comment.
I would, but RequiresDynamicCode can only go on methods, constructors, and classes.
aspnetcore/src/Shared/TrimmingAttributes.cs
Lines 16 to 17 in 0da8ea7
… other feature switch naming
BrennanConroy
left a comment
There was a problem hiding this comment.
Do you have a size comparison?
| @@ -0,0 +1,9 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
There was a problem hiding this comment.
It is an MSBuild project that doesn't have a language like a .csproj or a .vbproj does. It isn't technically part of MSBuild, but just a convention for a project that executes arbitrary MSBuild targets. (in this case, it generates a new .csproj from a template, publishes it, executes the pubilshed app, and verifies the app returned exit code 100.
Another example is the helixpublish.proj that publishes tests to helix.
aspnetcore/eng/common/helixpublish.proj
Lines 15 to 19 in 8046091
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Microbenchmarks", "src\SignalR\perf\Microbenchmarks\Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj", "{A6A95BEF-7E21-4D3D-921B-F77267219D27}" | ||
| EndProject | ||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests", "src\SignalR\server\SignalR\test\Microsoft.AspNetCore.SignalR.Tests.csproj", "{4DC9C494-9867-4319-937E-5FBC0E5F5A51}" | ||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests", "src\SignalR\server\SignalR\test\Microsoft.AspNetCore.SignalR.Tests\Microsoft.AspNetCore.SignalR.Tests.csproj", "{4DC9C494-9867-4319-937E-5FBC0E5F5A51}" |
There was a problem hiding this comment.
Add src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj?
There was a problem hiding this comment.
We don't put these in the .sln because they aren't "normal" projects, so you don't get intellisense editing or anything like that. Because they are self-contained executables, the UX for working on these tests is different than our other tests.
| "src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj", | ||
| "src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj", | ||
| "src\\SignalR\\server\\SignalR\\test\\Microsoft.AspNetCore.SignalR.Tests.csproj", | ||
| "src\\SignalR\\server\\SignalR\\test\\Microsoft.AspNetCore.SignalR.Tests\\Microsoft.AspNetCore.SignalR.Tests.csproj", |
There was a problem hiding this comment.
Add src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj?
| @@ -0,0 +1,84 @@ | |||
| // Licensed to the .NET Foundation under one or more agreements. | |||
There was a problem hiding this comment.
What mechanism is running this app?
There was a problem hiding this comment.
We have test infrastructure that runs all these *.TrimmingTests.proj and *.NativeAotTests.proj projects.
They run on the build machines on the "Test" legs of CI.
You can learn more about this at https://github.com/dotnet/aspnetcore/blob/main/docs/Trimming.md#validate-trimming-behavior.
Taking a Then adding the following to the app: Program.cs var builder = WebApplication.CreateSlimBuilder(args);
...
+builder.Services.AddSignalR();
+builder.Services.Configure<JsonHubProtocolOptions>(o =>
+{
+ o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});
var app = builder.Build();
...
+app.MapHub<ChatHub>("/chathub");
app.Run();ChatHub.cs using Microsoft.AspNetCore.SignalR;
public class ChatHub(ILogger<ChatHub> logger) : Hub
{
public async Task SendMessage(string user, string message)
{
logger.LogInformation("Received message from {user}: {message}", user, message);
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}Publishing again, the resulting .exe is So roughly
I think we are in a good spot right now though. I opened an issue for the Regex change, since that would have the biggest benefit. |
|
I was more asking how much this PR saves vs. not trimming SignalR. |
If I take the same project as above and make the following changes to the .csproj: <!-- <PublishAot>true</PublishAot> -->
<PublishSingleFile>true</PublishSingleFile>The resulting .exe is |
This is the SDK portion of changes needed to make SignalR compatible with native AOT. See dotnet/aspnetcore#56460.

Fix SignalR Server's usage of MakeGenericMethod when using a streaming reader (IAsyncEnumerable or ChannelReader) following the same approach as the client. Add a runtime check and throw an exception when trying to stream a ValueType in native AOT.
Adjust the public annotations:
Support trimming and AOT in DefaultHubDispatcher by adding a feature switch to turn off custom awaitable support. Update ObjectMethodExecutor to support trimming and AOT by a new method that doesn't look for custom awaitables, and uses reflection instead of Linq.Expressions. This will need a corresponding PR in https://github.com/dotnet/sdk to default the feature switch value when PublishTrimmed or PublishAot is set to
true.FYI - @agocke @MichalStrehovsky @sbomer - in case you want to take a look.