diff --git a/AspNetCore.sln b/AspNetCore.sln index 9164c1b75bba..7eba541ea0a0 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1102,7 +1102,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{5095E70C-6 EndProject 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}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specification.Tests", "Specification.Tests", "{245939DA-D68D-4B5C-A95E-D3B6923614FF}" EndProject diff --git a/eng/RequiresDelayedBuildProjects.props b/eng/RequiresDelayedBuildProjects.props index aca07fcc13f2..3c1626ba53bf 100644 --- a/eng/RequiresDelayedBuildProjects.props +++ b/eng/RequiresDelayedBuildProjects.props @@ -21,6 +21,7 @@ + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 3b28def7582c..6ba207ac1852 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -92,6 +92,8 @@ + + diff --git a/eng/testing/linker/SupportFiles/Directory.Build.targets b/eng/testing/linker/SupportFiles/Directory.Build.targets index dc209479dd85..8d7674b8923a 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.targets +++ b/eng/testing/linker/SupportFiles/Directory.Build.targets @@ -73,6 +73,8 @@ + + diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml index cd14d7bb7e37..4a18cf8fc813 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml @@ -1,36 +1,6 @@  - - - ILLink - IL2026 - member - M:Microsoft.AspNetCore.Components.BindConverter.FormatterDelegateCache.MakeTypeConverterFormatter``1 - - - ILLink - IL2026 - member - M:Microsoft.AspNetCore.Components.BindConverter.ParserDelegateCache.MakeTypeConverterConverter``1 - - - ILLink - IL2026 - member - M:Microsoft.AspNetCore.Components.RouteTableFactory.<GetRouteableComponents>g__GetRouteableComponents|4_0(System.Collections.Generic.List{System.Type},System.Reflection.Assembly) - - - ILLink - IL2062 - member - M:Microsoft.AspNetCore.Components.RouteTableFactory.Create(System.Collections.Generic.Dictionary{System.Type,System.String[]}) - - - ILLink - IL2067 - member - M:Microsoft.AspNetCore.Components.CascadingParameterState.CreateReflectedCascadingParameterInfos(System.Type) - + ILLink IL2072 @@ -43,47 +13,17 @@ member M:Microsoft.AspNetCore.Components.ComponentFactory.PerformPropertyInjection(System.IServiceProvider,Microsoft.AspNetCore.Components.IComponent) - - ILLink - IL2072 - member - M:Microsoft.AspNetCore.Components.DynamicComponent.SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView) - ILLink IL2072 member M:Microsoft.AspNetCore.Components.Reflection.ComponentProperties.SetProperties(Microsoft.AspNetCore.Components.ParameterView@,System.Object) - - ILLink - IL2077 - member - M:Microsoft.AspNetCore.Components.ComponentFactory.CreateInitializer(System.Type) - - - ILLink - IL2077 - member - M:Microsoft.AspNetCore.Components.LayoutView.<>c__DisplayClass13_0.<WrapInLayout>g__Render|0(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder) - - - ILLink - IL2080 - member - M:Microsoft.AspNetCore.Components.Reflection.MemberAssignment.<GetPropertiesIncludingInherited>d__0.MoveNext - ILLink IL2110 member M:Microsoft.AspNetCore.Components.RouteView.Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder) - - ILLink - IL2111 - member - M:Microsoft.AspNetCore.Components.RouteView.Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder) - \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml index 70f9e571648c..09ebb0306a2d 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.WarningSuppressions.xml @@ -1,6 +1,6 @@ - + - + ILLink IL2026 diff --git a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml index 35166cf65fd7..2da13e369808 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml +++ b/src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml @@ -1,72 +1,12 @@ - + - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime,Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo,System.String) - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.EndInvokeDotNetAfterTask(System.Threading.Tasks.Task,Microsoft.JSInterop.JSRuntime,Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo@) - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.Invoke(Microsoft.JSInterop.JSRuntime,Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo@,System.String) - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.ParseArguments(Microsoft.JSInterop.JSRuntime,System.String,System.String,System.Type[]) - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.JSRuntime.EndInvokeJS(System.Int64,System.Boolean,System.Text.Json.Utf8JsonReader@) - - - ILLink - IL2026 - member - M:Microsoft.JSInterop.JSRuntime.InvokeAsync``1(System.Int64,System.String,System.Threading.CancellationToken,System.Object[]) - - - ILLink - IL2055 - member - M:Microsoft.JSInterop.Infrastructure.DotNetObjectReferenceJsonConverterFactory.CreateConverter(System.Type,System.Text.Json.JsonSerializerOptions) - + ILLink IL2065 member M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.ScanAssemblyForCallableMethods(Microsoft.JSInterop.Infrastructure.DotNetDispatcher.AssemblyKey) - - ILLink - IL2091 - member - M:Microsoft.JSInterop.JSObjectReferenceExtensions.<InvokeAsync>d__4`1.MoveNext - - - ILLink - IL2091 - member - M:Microsoft.JSInterop.JSRuntime.<InvokeAsync>d__16`1.MoveNext - - - ILLink - IL2091 - member - M:Microsoft.JSInterop.JSRuntimeExtensions.<InvokeAsync>d__4`1.MoveNext - ILLink IL2111 @@ -74,4 +14,4 @@ M:Microsoft.JSInterop.Infrastructure.DotNetDispatcher.GetCachedMethodInfo(Microsoft.JSInterop.Infrastructure.IDotNetObjectReference,System.String) - + \ No newline at end of file diff --git a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs index 8de064da2216..b24d903c3ebd 100644 --- a/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs +++ b/src/Shared/ObjectMethodExecutor/ObjectMethodExecutor.cs @@ -7,11 +7,10 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; namespace Microsoft.Extensions.Internal; -[RequiresUnreferencedCode("ObjectMethodExecutor performs reflection on arbitrary types.")] -[RequiresDynamicCode("ObjectMethodExecutor performs reflection on arbitrary types.")] internal sealed class ObjectMethodExecutor { private readonly object?[]? _parameterDefaultValues; @@ -28,7 +27,7 @@ internal sealed class ObjectMethodExecutor typeof(Action) // unsafeOnCompletedMethod })!; - private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object?[]? parameterDefaultValues) + private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) { ArgumentNullException.ThrowIfNull(methodInfo); @@ -36,7 +35,13 @@ private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, obj MethodParameters = methodInfo.GetParameters(); TargetTypeInfo = targetTypeInfo; MethodReturnType = methodInfo.ReturnType; + } + [RequiresUnreferencedCode("ObjectMethodExecutor performs reflection on arbitrary types.")] + [RequiresDynamicCode("ObjectMethodExecutor performs reflection on arbitrary types.")] + private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object?[]? parameterDefaultValues) + : this(methodInfo, targetTypeInfo) + { var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo); IsMethodAsync = isAwaitable; @@ -55,6 +60,27 @@ private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, obj _parameterDefaultValues = parameterDefaultValues; } + private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, bool isTrimAotCompatible) + : this(methodInfo, targetTypeInfo) + { + Debug.Assert(isTrimAotCompatible, "isTrimAotCompatible should always be true."); + + var isAwaitable = IsTaskType(MethodReturnType, out var resultType); + + IsMethodAsync = isAwaitable; + AsyncResultType = isAwaitable ? resultType : null; + + // Upstream code may prefer to use the sync-executor even for async methods, because if it knows + // that the result is a specific Task where T is known, then it can directly cast to that type + // and await it without the extra heap allocations involved in the _executorAsync code path. + _executor = methodInfo.Invoke; + + if (IsMethodAsync) + { + _executorAsync = GetExecutorAsyncTrimAotCompatible(methodInfo, AsyncResultType!); + } + } + private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, object?[]? parameters); private delegate object? MethodExecutor(object target, object?[]? parameters); @@ -74,11 +100,15 @@ private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, obj public bool IsMethodAsync { get; } + [RequiresUnreferencedCode("ObjectMethodExecutor performs reflection on arbitrary types.")] + [RequiresDynamicCode("ObjectMethodExecutor performs reflection on arbitrary types.")] public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) { return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null); } + [RequiresUnreferencedCode("ObjectMethodExecutor performs reflection on arbitrary types.")] + [RequiresDynamicCode("ObjectMethodExecutor performs reflection on arbitrary types.")] public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, object?[] parameterDefaultValues) { ArgumentNullException.ThrowIfNull(parameterDefaultValues); @@ -86,6 +116,19 @@ public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo target return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues); } + /// + /// Creates an ObjectMethodExecutor that is compatible with trimming and Ahead-of-Time (AOT) compilation. + /// + /// + /// The difference between this method and is that + /// this method doesn't support custom awaitables and Task{unit} in F#. It only supports Task, Task{T}, ValueTask, and ValueTask{T} + /// as async methods. + /// + public static ObjectMethodExecutor CreateTrimAotCompatible(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + return new ObjectMethodExecutor(methodInfo, targetTypeInfo, isTrimAotCompatible: true); + } + /// /// Executes the configured method on . This can be used whether or not /// the configured method is asynchronous. @@ -123,6 +166,9 @@ public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo target /// of it, and if it is, it will have to be boxed so the calling code can reference it as an object). /// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling /// code doesn't know what type it's going to be). + /// + /// Note if was used to create the ObjectMethodExecutor, only the + /// built-in Task types are supported and not custom awaitables. /// /// The object whose method is to be executed. /// Parameters to pass to the method. @@ -336,4 +382,146 @@ private static MethodExecutorAsync GetExecutorAsync( var lambda = Expression.Lambda(returnValueExpression, targetParameter, parametersParameter); return lambda.Compile(); } + + private static readonly MethodInfo _taskGetAwaiterMethodInfo = typeof(Task<>).GetMethod("GetAwaiter")!; + private static readonly MethodInfo _taskAwaiterGetIsCompletedMethodInfo = typeof(TaskAwaiter<>).GetMethod("get_IsCompleted")!; + private static readonly MethodInfo _taskAwaiterGetResultMethodInfo = typeof(TaskAwaiter<>).GetMethod("GetResult")!; + private static readonly MethodInfo _taskAwaiterOnCompletedMethodInfo = typeof(TaskAwaiter<>).GetMethod("OnCompleted")!; + private static readonly MethodInfo _taskAwaiterUnsafeOnCompletedMethodInfo = typeof(TaskAwaiter<>).GetMethod("UnsafeOnCompleted")!; + + private static readonly MethodInfo _valueTaskGetAwaiterMethodInfo = typeof(ValueTask<>).GetMethod("GetAwaiter")!; + private static readonly MethodInfo _valueTaskAwaiterGetIsCompletedMethodInfo = typeof(ValueTaskAwaiter<>).GetMethod("get_IsCompleted")!; + private static readonly MethodInfo _valueTaskAwaiterGetResultMethodInfo = typeof(ValueTaskAwaiter<>).GetMethod("GetResult")!; + private static readonly MethodInfo _valueTaskAwaiterOnCompletedMethodInfo = typeof(ValueTaskAwaiter<>).GetMethod("OnCompleted")!; + private static readonly MethodInfo _valueTaskAwaiterUnsafeOnCompletedMethodInfo = typeof(ValueTaskAwaiter<>).GetMethod("UnsafeOnCompleted")!; + + private static bool IsTaskType(Type methodReturnType, [NotNullWhen(true)] out Type? resultType) + { + if (methodReturnType == typeof(Task) || methodReturnType == typeof(ValueTask)) + { + resultType = typeof(void); + return true; + } + + if (methodReturnType.IsGenericType && methodReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + resultType = methodReturnType.GetGenericArguments()[0]; + return true; + } + + var currentType = methodReturnType; + while (currentType is not null) + { + if (currentType == typeof(Task)) + { + resultType = typeof(void); + return true; + } + + if (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == typeof(Task<>)) + { + var taskGetAwaiterMethodInfo = (MethodInfo)methodReturnType.GetMemberWithSameMetadataDefinitionAs(_taskGetAwaiterMethodInfo); + var taskAwaiterGetResultMethodInfo = (MethodInfo)taskGetAwaiterMethodInfo.ReturnType.GetMemberWithSameMetadataDefinitionAs(_taskAwaiterGetResultMethodInfo); + + resultType = taskAwaiterGetResultMethodInfo.ReturnType; + return true; + } + + currentType = currentType.BaseType; + } + + resultType = null; + return false; + } + + private static MethodExecutorAsync? GetExecutorAsyncTrimAotCompatible(MethodInfo methodInfo, Type asyncResultType) + { + var methodReturnType = methodInfo.ReturnType; + if (asyncResultType == typeof(void)) + { + if (methodReturnType == typeof(ValueTask)) + { + return (target, args) => + { + return new ObjectMethodExecutorAwaitable( + methodInfo.Invoke(target, args), + (awaitable) => ((ValueTask)awaitable).GetAwaiter(), + (awaiter) => ((ValueTaskAwaiter)awaiter).IsCompleted, + (awaiter) => + { + ((ValueTaskAwaiter)awaiter).GetResult(); + return null; + }, + (awaiter, continuation) => + { + ((ValueTaskAwaiter)awaiter).OnCompleted(continuation); + }, + (awaiter, continuation) => + { + ((ValueTaskAwaiter)awaiter).UnsafeOnCompleted(continuation); + }); + }; + } + + // The method must return Task, or a derived type that isn't Task + return (target, args) => + { + return new ObjectMethodExecutorAwaitable( + methodInfo.Invoke(target, args), + (awaitable) => ((Task)awaitable).GetAwaiter(), + (awaiter) => ((TaskAwaiter)awaiter).IsCompleted, + (awaiter) => + { + ((TaskAwaiter)awaiter).GetResult(); + return null; + }, + (awaiter, continuation) => + { + ((TaskAwaiter)awaiter).OnCompleted(continuation); + }, + (awaiter, continuation) => + { + ((TaskAwaiter)awaiter).UnsafeOnCompleted(continuation); + }); + }; + } + + if (methodReturnType.IsGenericType && methodReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + return (target, args) => + { + return new ObjectMethodExecutorAwaitable( + methodInfo.Invoke(target, args), + (awaitable) => ((MethodInfo)awaitable.GetType().GetMemberWithSameMetadataDefinitionAs(_valueTaskGetAwaiterMethodInfo)).Invoke(awaitable, Array.Empty()), + (awaiter) => (bool)((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_valueTaskAwaiterGetIsCompletedMethodInfo)).Invoke(awaiter, Array.Empty())!, + (awaiter) => ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_valueTaskAwaiterGetResultMethodInfo)).Invoke(awaiter, Array.Empty())!, + (awaiter, continuation) => + { + ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_valueTaskAwaiterOnCompletedMethodInfo)).Invoke(awaiter, [continuation]); + }, + (awaiter, continuation) => + { + ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_valueTaskAwaiterUnsafeOnCompletedMethodInfo)).Invoke(awaiter, [continuation]); + }); + }; + } + + // The method must return a Task or a derived type + return (target, args) => + { + return new ObjectMethodExecutorAwaitable( + methodInfo.Invoke(target, args), + (awaitable) => ((MethodInfo)awaitable.GetType().GetMemberWithSameMetadataDefinitionAs(_taskGetAwaiterMethodInfo)).Invoke(awaitable, Array.Empty()), + (awaiter) => (bool)((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_taskAwaiterGetIsCompletedMethodInfo)).Invoke(awaiter, Array.Empty())!, + (awaiter) => ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_taskAwaiterGetResultMethodInfo)).Invoke(awaiter, Array.Empty())!, + (awaiter, continuation) => + { + ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_taskAwaiterOnCompletedMethodInfo)).Invoke(awaiter, [continuation]); + }, + (awaiter, continuation) => + { + ((MethodInfo)awaiter.GetType().GetMemberWithSameMetadataDefinitionAs(_taskAwaiterUnsafeOnCompletedMethodInfo)).Invoke(awaiter, [continuation]); + }); + }; + } } diff --git a/src/SignalR/SignalR.slnf b/src/SignalR/SignalR.slnf index b4e7e035fe51..28592c935943 100644 --- a/src/SignalR/SignalR.slnf +++ b/src/SignalR/SignalR.slnf @@ -68,7 +68,7 @@ "src\\SignalR\\samples\\WebSocketSample\\WebSocketSample.csproj", "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", "src\\SignalR\\server\\Specification.Tests\\src\\Microsoft.AspNetCore.SignalR.Specification.Tests.csproj", "src\\SignalR\\server\\StackExchangeRedis\\src\\Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj", "src\\SignalR\\server\\StackExchangeRedis\\test\\Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.csproj", diff --git a/src/SignalR/common/Protocols.MessagePack/src/MessagePackProtocolDependencyInjectionExtensions.cs b/src/SignalR/common/Protocols.MessagePack/src/MessagePackProtocolDependencyInjectionExtensions.cs index 7a815f30d7db..26cefd96bb60 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/MessagePackProtocolDependencyInjectionExtensions.cs +++ b/src/SignalR/common/Protocols.MessagePack/src/MessagePackProtocolDependencyInjectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -21,6 +22,7 @@ public static class MessagePackProtocolDependencyInjectionExtensions /// /// The representing the SignalR server to add MessagePack protocol support to. /// The value of + [RequiresUnreferencedCode("MessagePack does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static TBuilder AddMessagePackProtocol(this TBuilder builder) where TBuilder : ISignalRBuilder => AddMessagePackProtocol(builder, _ => { }); @@ -33,6 +35,7 @@ public static TBuilder AddMessagePackProtocol(this TBuilder builder) w /// The representing the SignalR server to add MessagePack protocol support to. /// A delegate that can be used to configure the /// The value of + [RequiresUnreferencedCode("MessagePack does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static TBuilder AddMessagePackProtocol(this TBuilder builder, Action configure) where TBuilder : ISignalRBuilder { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj b/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj index b90702bc6c8b..be6f0bab32e3 100644 --- a/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj +++ b/src/SignalR/common/Protocols.MessagePack/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj index f905437355d7..6b621b28bc00 100644 --- a/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj +++ b/src/SignalR/common/Protocols.NewtonsoftJson/src/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/SignalR/common/Protocols.NewtonsoftJson/src/NewtonsoftJsonProtocolDependencyInjectionExtensions.cs b/src/SignalR/common/Protocols.NewtonsoftJson/src/NewtonsoftJsonProtocolDependencyInjectionExtensions.cs index 03c97811c1f1..7e0030fa2a28 100644 --- a/src/SignalR/common/Protocols.NewtonsoftJson/src/NewtonsoftJsonProtocolDependencyInjectionExtensions.cs +++ b/src/SignalR/common/Protocols.NewtonsoftJson/src/NewtonsoftJsonProtocolDependencyInjectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -21,6 +22,7 @@ public static class NewtonsoftJsonProtocolDependencyInjectionExtensions /// /// The representing the SignalR server to add JSON protocol support to. /// The value of + [RequiresUnreferencedCode("Newtonsoft.Json does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static TBuilder AddNewtonsoftJsonProtocol(this TBuilder builder) where TBuilder : ISignalRBuilder => AddNewtonsoftJsonProtocol(builder, _ => { }); @@ -33,6 +35,7 @@ public static TBuilder AddNewtonsoftJsonProtocol(this TBuilder builder /// The representing the SignalR server to add JSON protocol support to. /// A delegate that can be used to configure the /// The value of + [RequiresUnreferencedCode("Newtonsoft.Json does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static TBuilder AddNewtonsoftJsonProtocol(this TBuilder builder, Action configure) where TBuilder : ISignalRBuilder { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/SignalR/common/Shared/ReflectionHelper.cs b/src/SignalR/common/Shared/ReflectionHelper.cs index 70351533572f..1713ffa06da7 100644 --- a/src/SignalR/common/Shared/ReflectionHelper.cs +++ b/src/SignalR/common/Shared/ReflectionHelper.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Channels; namespace Microsoft.AspNetCore.SignalR; @@ -17,7 +15,7 @@ internal static class ReflectionHelper // and 'stream' types from the client are allowed to inherit from accepted 'stream' types public static bool IsStreamingType(Type type, bool mustBeDirectType = false) { - // TODO #2594 - add Streams here, to make sending files easy + // TODO https://github.com/dotnet/aspnetcore/issues/5316 - add Streams here, to make sending files easy if (IsIAsyncEnumerable(type)) { diff --git a/src/SignalR/server/Core/src/Hub.cs b/src/SignalR/server/Core/src/Hub.cs index acf89fffc961..c32c75782847 100644 --- a/src/SignalR/server/Core/src/Hub.cs +++ b/src/SignalR/server/Core/src/Hub.cs @@ -1,6 +1,8 @@ // 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; + namespace Microsoft.AspNetCore.SignalR; /// @@ -8,6 +10,8 @@ namespace Microsoft.AspNetCore.SignalR; /// public abstract class Hub : IDisposable { + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedMembers = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods; + private bool _disposed; private IHubCallerClients _clients = default!; private HubCallerContext _context = default!; diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 342cbedb8dae..a56cd9a90835 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.SignalR.Internal; @@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.SignalR; /// /// Handles incoming connections and implements the SignalR Hub Protocol. /// -public class HubConnectionHandler : ConnectionHandler where THub : Hub +public class HubConnectionHandler<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> : ConnectionHandler where THub : Hub { private readonly HubLifetimeManager _lifetimeManager; private readonly ILoggerFactory _loggerFactory; diff --git a/src/SignalR/server/Core/src/Hub`T.cs b/src/SignalR/server/Core/src/Hub`T.cs index fc7c32edd388..7c3dcf83a2d5 100644 --- a/src/SignalR/server/Core/src/Hub`T.cs +++ b/src/SignalR/server/Core/src/Hub`T.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.SignalR.Internal; namespace Microsoft.AspNetCore.SignalR; @@ -9,7 +10,8 @@ namespace Microsoft.AspNetCore.SignalR; /// A base class for a strongly typed SignalR hub. /// /// The type of client. -public abstract class Hub : Hub where T : class +[RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] +public abstract class Hub<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : Hub where T : class { private IHubCallerClients? _clients; diff --git a/src/SignalR/server/Core/src/IHubActivator.cs b/src/SignalR/server/Core/src/IHubActivator.cs index f76b10763cab..b8f84c2b0903 100644 --- a/src/SignalR/server/Core/src/IHubActivator.cs +++ b/src/SignalR/server/Core/src/IHubActivator.cs @@ -1,13 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; + namespace Microsoft.AspNetCore.SignalR; /// /// A activator abstraction. /// /// The hub type. -public interface IHubActivator where THub : Hub +public interface IHubActivator<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> where THub : Hub { /// /// Creates a hub. diff --git a/src/SignalR/server/Core/src/IHubContext`T.cs b/src/SignalR/server/Core/src/IHubContext`T.cs index 8cf53f1c14f1..438c49ced545 100644 --- a/src/SignalR/server/Core/src/IHubContext`T.cs +++ b/src/SignalR/server/Core/src/IHubContext`T.cs @@ -1,19 +1,25 @@ // 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; + namespace Microsoft.AspNetCore.SignalR; /// /// A context abstraction for a hub. /// -public interface IHubContext +public interface IHubContext where THub : Hub where T : class { /// /// Gets a that can be used to invoke methods on clients connected to the hub. /// - IHubClients Clients { get; } + IHubClients Clients + { + [RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] + get; + } /// /// Gets a that can be used to add and remove connections to named groups. diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubActivator.cs b/src/SignalR/server/Core/src/Internal/DefaultHubActivator.cs index 0d9b91178f0c..f071633dd938 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubActivator.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubActivator.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed class DefaultHubActivator : IHubActivator where THub : Hub +internal sealed class DefaultHubActivator<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> : IHubActivator where THub : Hub { // Object factory for THub instances private static readonly Lazy _objectFactory = new Lazy(() => ActivatorUtilities.CreateFactory(typeof(THub), Type.EmptyTypes)); diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs index b2e54590e2e2..ece981d2c489 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Security.Claims; @@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed partial class DefaultHubDispatcher : HubDispatcher where THub : Hub +internal sealed partial class DefaultHubDispatcher<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> : HubDispatcher where THub : Hub { private static readonly string _fullHubName = typeof(THub).FullName ?? typeof(THub).Name; @@ -31,6 +32,12 @@ internal sealed partial class DefaultHubDispatcher : HubDispatcher w private readonly Func? _onDisconnectedMiddleware; private readonly HubLifetimeManager _hubLifetimeManager; + [FeatureSwitchDefinition("Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported")] + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + private static bool IsCustomAwaitableSupported { get; } = + AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported", out bool customAwaitableSupport) ? customAwaitableSupport : true; + public DefaultHubDispatcher(IServiceScopeFactory serviceScopeFactory, IHubContext hubContext, bool enableDetailedErrors, bool disableImplicitFromServiceParameters, ILogger> logger, List? hubFilters, HubLifetimeManager lifetimeManager) { @@ -761,7 +768,10 @@ private void DiscoverHubMethods(bool disableImplicitFromServiceParameters) throw new NotSupportedException($"Duplicate definitions of '{methodName}'. Overloading is not supported."); } - var executor = ObjectMethodExecutor.Create(methodInfo, hubTypeInfo); + var executor = IsCustomAwaitableSupported + ? CreateObjectMethodExecutor(methodInfo, hubTypeInfo) + : ObjectMethodExecutor.CreateTrimAotCompatible(methodInfo, hubTypeInfo); + var authorizeAttributes = methodInfo.GetCustomAttributes(inherit: true); _methods[methodName] = new HubMethodDescriptor(executor, serviceProviderIsService, authorizeAttributes); _cachedMethodNames.Add(methodName); @@ -770,6 +780,11 @@ private void DiscoverHubMethods(bool disableImplicitFromServiceParameters) } } + [RequiresUnreferencedCode("Using SignalR with 'Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported=true' is not trim compatible.")] + [RequiresDynamicCode("Using SignalR with 'Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported=true' is not native AOT compatible.")] + private static ObjectMethodExecutor CreateObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetType) + => ObjectMethodExecutor.Create(methodInfo, targetType); + public override IReadOnlyList GetParameterTypes(string methodName) { if (!_methods.TryGetValue(methodName, out var descriptor)) diff --git a/src/SignalR/server/Core/src/Internal/HubClients`T.cs b/src/SignalR/server/Core/src/Internal/HubClients`T.cs index e168174b6464..360ae3f0e153 100644 --- a/src/SignalR/server/Core/src/Internal/HubClients`T.cs +++ b/src/SignalR/server/Core/src/Internal/HubClients`T.cs @@ -1,9 +1,12 @@ // 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; + namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed class HubClients : IHubClients where THub : Hub +[RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] +internal sealed class HubClients : IHubClients where THub : Hub { private readonly HubLifetimeManager _lifetimeManager; diff --git a/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs b/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs index 9b844929b951..512897b5489a 100644 --- a/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs +++ b/src/SignalR/server/Core/src/Internal/HubConnectionBinder.cs @@ -1,9 +1,11 @@ // 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; + namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed class HubConnectionBinder : IInvocationBinder where THub : Hub +internal sealed class HubConnectionBinder<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> : IInvocationBinder where THub : Hub { private readonly HubDispatcher _dispatcher; private readonly HubConnectionContext _connection; diff --git a/src/SignalR/server/Core/src/Internal/HubContext`T.cs b/src/SignalR/server/Core/src/Internal/HubContext`T.cs index e7e154d7e764..14f37f934677 100644 --- a/src/SignalR/server/Core/src/Internal/HubContext`T.cs +++ b/src/SignalR/server/Core/src/Internal/HubContext`T.cs @@ -1,9 +1,12 @@ // 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; + namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed class HubContext : IHubContext +[RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] +internal sealed class HubContext : IHubContext where THub : Hub where T : class { diff --git a/src/SignalR/server/Core/src/Internal/HubDispatcher.cs b/src/SignalR/server/Core/src/Internal/HubDispatcher.cs index efae161bddd8..0edfa681bfdb 100644 --- a/src/SignalR/server/Core/src/Internal/HubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/HubDispatcher.cs @@ -1,11 +1,12 @@ // 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 Microsoft.AspNetCore.SignalR.Protocol; namespace Microsoft.AspNetCore.SignalR.Internal; -internal abstract class HubDispatcher where THub : Hub +internal abstract class HubDispatcher<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub> where THub : Hub { public abstract Task OnConnectedAsync(HubConnectionContext connection); public abstract Task OnDisconnectedAsync(HubConnectionContext connection, Exception? exception); diff --git a/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs b/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs index b10e83eeecd3..01ec440004eb 100644 --- a/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs +++ b/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs @@ -1,9 +1,12 @@ // 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; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Channels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Metadata; @@ -35,27 +38,22 @@ public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IServiceProvider ? MethodExecutor.AsyncResultType! : MethodExecutor.MethodReturnType; - foreach (var returnType in NonAsyncReturnType.GetInterfaces().Concat(NonAsyncReturnType.AllBaseTypes())) + var asyncEnumerableType = ReflectionHelper.GetIAsyncEnumerableInterface(NonAsyncReturnType); + if (asyncEnumerableType is not null) { - if (!returnType.IsGenericType) - { - continue; - } - - var openReturnType = returnType.GetGenericTypeDefinition(); - - if (openReturnType == typeof(IAsyncEnumerable<>)) - { - StreamReturnType = returnType.GetGenericArguments()[0]; - _makeCancelableEnumeratorMethodInfo = MakeCancelableAsyncEnumeratorMethod; - break; - } - - if (openReturnType == typeof(ChannelReader<>)) + StreamReturnType = ValidateStreamType(asyncEnumerableType.GetGenericArguments()[0]); + _makeCancelableEnumeratorMethodInfo = MakeCancelableAsyncEnumeratorMethod; + } + else + { + foreach (var returnType in NonAsyncReturnType.AllBaseTypes()) { - StreamReturnType = returnType.GetGenericArguments()[0]; - _makeCancelableEnumeratorMethodInfo = MakeAsyncEnumeratorFromChannelMethod; - break; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ChannelReader<>)) + { + StreamReturnType = ValidateStreamType(returnType.GetGenericArguments()[0]); + _makeCancelableEnumeratorMethodInfo = MakeAsyncEnumeratorFromChannelMethod; + break; + } } } @@ -75,7 +73,7 @@ public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IServiceProvider StreamingParameters = new List(); } - StreamingParameters.Add(p.ParameterType.GetGenericArguments()[0]); + StreamingParameters.Add(ValidateStreamType(p.ParameterType.GetGenericArguments()[0])); HasSyntheticArguments = true; return false; } @@ -205,15 +203,25 @@ public object GetService(IServiceProvider serviceProvider, int index, Type param public IAsyncEnumerator FromReturnedStream(object stream, CancellationToken cancellationToken) { - // there is the potential for compile to be called times but this has no harmful effect other than perf + // there is the potential for _makeCancelableEnumerator to be set multiple times but this has no harmful effect other than startup perf if (_makeCancelableEnumerator == null) { - _makeCancelableEnumerator = CompileConvertToEnumerator(_makeCancelableEnumeratorMethodInfo!, StreamReturnType!); + if (RuntimeFeature.IsDynamicCodeSupported) + { + _makeCancelableEnumerator = CompileConvertToEnumerator(_makeCancelableEnumeratorMethodInfo!, StreamReturnType!); + } + else + { + _makeCancelableEnumerator = ConvertToEnumeratorWithReflection(_makeCancelableEnumeratorMethodInfo!, StreamReturnType!); + } } return _makeCancelableEnumerator.Invoke(stream, cancellationToken); } + [UnconditionalSuppressMessage("Trimming", "IL2060:MakeGenericMethod", + Justification = "The adapter methods passed into here (MakeCancelableAsyncEnumerator and MakeAsyncEnumeratorFromChannel) don't have trimming annotations.")] + [RequiresDynamicCode("Calls MakeGenericMethod with types that may be ValueTypes")] private static Func> CompileConvertToEnumerator(MethodInfo adapterMethodInfo, Type streamReturnType) { // This will call one of two adapter methods to wrap the passed in streamable value into an IAsyncEnumerable: @@ -239,6 +247,21 @@ private static Func> Compile return lambda.Compile(); } + [UnconditionalSuppressMessage("Trimming", "IL2060:MakeGenericMethod", + Justification = "The adapter methods passed into here (MakeCancelableAsyncEnumerator and MakeAsyncEnumeratorFromChannel) don't have trimming annotations.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "There is a runtime check for ValueType streaming item type when PublishAot=true. Developers will get an exception in this situation before publishing.")] + private static Func> ConvertToEnumeratorWithReflection(MethodInfo adapterMethodInfo, Type streamReturnType) + { + Debug.Assert(!streamReturnType.IsValueType, "ValidateStreamType will throw during the ctor if the streamReturnType is a ValueType when PublishAot=true."); + + var genericAdapterMethodInfo = adapterMethodInfo.MakeGenericMethod(streamReturnType); + return (stream, cancellationToken) => + { + return (IAsyncEnumerator)genericAdapterMethodInfo.Invoke(null, [stream, cancellationToken])!; + }; + } + private static Type GetServiceType(Type type) { // IServiceProviderIsService will special case IEnumerable<> and always return true @@ -252,4 +275,17 @@ private static Type GetServiceType(Type type) return type; } + + private Type ValidateStreamType(Type streamType) + { + if (!RuntimeFeature.IsDynamicCodeSupported && streamType.IsValueType) + { + // NativeAOT apps are not able to stream IAsyncEnumerable and ChannelReader of ValueTypes + // since we cannot create AsyncEnumerableAdapters.MakeCancelableAsyncEnumerator and AsyncEnumerableAdapters.MakeAsyncEnumeratorFromChannel methods with a generic ValueType. + var methodInfo = MethodExecutor.MethodInfo; + throw new InvalidOperationException($"Unable to stream an item with type '{streamType}' on method '{methodInfo.DeclaringType}.{methodInfo.Name}' because it is a ValueType. Native code to support streaming this ValueType will not be available with native AOT."); + } + + return streamType; + } } diff --git a/src/SignalR/server/Core/src/Internal/HubReflectionHelper.cs b/src/SignalR/server/Core/src/Internal/HubReflectionHelper.cs index d54fb6cfe779..a5f6332e7aba 100644 --- a/src/SignalR/server/Core/src/Internal/HubReflectionHelper.cs +++ b/src/SignalR/server/Core/src/Internal/HubReflectionHelper.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Reflection; @@ -8,24 +9,12 @@ namespace Microsoft.AspNetCore.SignalR.Internal; internal static class HubReflectionHelper { - private static readonly Type[] _excludeInterfaces = new[] { typeof(IDisposable) }; - - public static IEnumerable GetHubMethods(Type hubType) + public static IEnumerable GetHubMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type hubType) { var methods = hubType.GetMethods(BindingFlags.Public | BindingFlags.Instance); - var allInterfaceMethods = _excludeInterfaces.SelectMany(i => GetInterfaceMethods(hubType, i)); - - return methods.Except(allInterfaceMethods).Where(IsHubMethod); - } - - private static IEnumerable GetInterfaceMethods(Type type, Type iface) - { - if (!iface.IsAssignableFrom(type)) - { - return Enumerable.Empty(); - } + var excludedInterfaceMethods = hubType.GetInterfaceMap(typeof(IDisposable)).TargetMethods; - return type.GetInterfaceMap(iface).TargetMethods; + return methods.Except(excludedInterfaceMethods).Where(IsHubMethod); } private static bool IsHubMethod(MethodInfo methodInfo) diff --git a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs index 985dc14e96bf..a1afdde56598 100644 --- a/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs +++ b/src/SignalR/server/Core/src/Internal/TypedClientBuilder.cs @@ -1,13 +1,15 @@ // 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.Linq; using System.Reflection; using System.Reflection.Emit; namespace Microsoft.AspNetCore.SignalR.Internal; -internal static class TypedClientBuilder +[RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] +internal static class TypedClientBuilder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> { private const string ClientModuleName = "Microsoft.AspNetCore.SignalR.TypedClientBuilder"; @@ -44,6 +46,7 @@ private static Func GenerateClientBuilder() return (Func)factoryMethod!.CreateDelegate(typeof(Func)); } + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] private static Type GenerateInterfaceImplementation(ModuleBuilder moduleBuilder) { var name = ClientModuleName + "." + typeof(T).Name + "Impl"; @@ -64,14 +67,19 @@ private static Type GenerateInterfaceImplementation(ModuleBuilder moduleBuilder) BuildMethod(type, method, proxyField); } - return type.CreateTypeInfo()!; + return type.CreateType(); } - private static IEnumerable GetAllInterfaceMethods(Type interfaceType) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", + Justification = "interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces will be preserved as well. https://github.com/mono/linker/issues/1731 tracks not emitting warnings here.")] + private static IEnumerable GetAllInterfaceMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { foreach (var parent in interfaceType.GetInterfaces()) { + // interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces will be preserved as well. https://github.com/mono/linker/issues/1731 tracks not emitting warnings here. +#pragma warning disable IL2072 foreach (var parentMethod in GetAllInterfaceMethods(parent)) +#pragma warning restore IL2072 { yield return parentMethod; } @@ -239,7 +247,9 @@ private static void BuildFactoryMethod(TypeBuilder type, ConstructorInfo ctor) generator.Emit(OpCodes.Ret); // Return the typed client } - private static void VerifyInterface(Type interfaceType) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062:UnrecognizedReflectionPattern", + Justification = "interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces will be preserved as well. https://github.com/mono/linker/issues/1731 tracks not emitting warnings here.")] + private static void VerifyInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) { if (!interfaceType.IsInterface) { @@ -263,7 +273,10 @@ private static void VerifyInterface(Type interfaceType) foreach (var parent in interfaceType.GetInterfaces()) { + // interfaceType is annotated as preserve All members, so any Types returned from GetInterfaces will be preserved as well. https://github.com/mono/linker/issues/1731 tracks not emitting warnings here. +#pragma warning disable IL2072 VerifyInterface(parent); +#pragma warning restore IL2072 } } diff --git a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs index 723d09e7fbd8..e0b9a6e90d5b 100644 --- a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs +++ b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs @@ -1,9 +1,12 @@ // 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; + namespace Microsoft.AspNetCore.SignalR.Internal; -internal sealed class TypedHubClients : IHubCallerClients +[RequiresDynamicCode("Creating a proxy instance requires generating code at runtime")] +internal sealed class TypedHubClients<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : IHubCallerClients { private readonly IHubCallerClients _hubClients; diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.WarningSuppressions.xml b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.WarningSuppressions.xml new file mode 100644 index 000000000000..6923b9ee4686 --- /dev/null +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.WarningSuppressions.xml @@ -0,0 +1,12 @@ + + + + + ILLink + IL2026 + member + M:Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.DiscoverHubMethods(System.Boolean) + This warning is left in the product so developers get an ILLink warning when trimming an app when Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported=true. + + + diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj index b4552ea0c9f6..f4276f93dcdb 100644 --- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj @@ -7,6 +7,7 @@ Microsoft.AspNetCore.SignalR false enable + true diff --git a/src/SignalR/server/Core/src/SignalRConnectionBuilderExtensions.cs b/src/SignalR/server/Core/src/SignalRConnectionBuilderExtensions.cs index 9f3ef3105d28..6a5624180b28 100644 --- a/src/SignalR/server/Core/src/SignalRConnectionBuilderExtensions.cs +++ b/src/SignalR/server/Core/src/SignalRConnectionBuilderExtensions.cs @@ -12,15 +12,13 @@ namespace Microsoft.AspNetCore.SignalR; /// public static class SignalRConnectionBuilderExtensions { - private const DynamicallyAccessedMemberTypes HubAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods; - /// /// Configure the connection to host the specified type. /// /// The type to host on the connection. /// The connection to configure. /// The same instance of the for chaining. - public static IConnectionBuilder UseHub<[DynamicallyAccessedMembers(HubAccessibility)] THub>(this IConnectionBuilder connectionBuilder) where THub : Hub + public static IConnectionBuilder UseHub<[DynamicallyAccessedMembers(Hub.DynamicallyAccessedMembers)] THub>(this IConnectionBuilder connectionBuilder) where THub : Hub { var marker = connectionBuilder.ApplicationServices.GetService(typeof(SignalRCoreMarkerService)); if (marker == null) diff --git a/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs index 317e11a63ff3..51b5746fd96e 100644 --- a/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/Core/src/SignalRDependencyInjectionExtensions.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -24,7 +25,7 @@ public static ISignalRServerBuilder AddSignalRCore(this IServiceCollection servi services.TryAddSingleton(typeof(HubLifetimeManager<>), typeof(DefaultHubLifetimeManager<>)); services.TryAddSingleton(typeof(IHubProtocolResolver), typeof(DefaultHubProtocolResolver)); services.TryAddSingleton(typeof(IHubContext<>), typeof(HubContext<>)); - services.TryAddSingleton(typeof(IHubContext<,>), typeof(HubContext<,>)); + AddTypedHubContext(services); services.TryAddSingleton(typeof(HubConnectionHandler<>), typeof(HubConnectionHandler<>)); services.TryAddSingleton(typeof(IUserIdProvider), typeof(DefaultUserIdProvider)); services.TryAddSingleton(typeof(HubDispatcher<>), typeof(DefaultHubDispatcher<>)); @@ -36,5 +37,13 @@ public static ISignalRServerBuilder AddSignalRCore(this IServiceCollection servi var builder = new SignalRServerBuilder(services); builder.AddJsonProtocol(); return builder; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", + Justification = "HubContext<,>'s ctor creates a HubClients<,> instance, which generates code dynamically. " + + "The property that accesses the HubClients<,> is annotated as RequiresDynamicCode on IHubContext<,>, so developers will get a warning when using it.")] + static void AddTypedHubContext(IServiceCollection services) + { + services.TryAddSingleton(typeof(IHubContext<,>), typeof(HubContext<,>)); + } } } diff --git a/src/SignalR/server/Core/src/StreamTracker.cs b/src/SignalR/server/Core/src/StreamTracker.cs index b317243c7df9..81fea34566de 100644 --- a/src/SignalR/server/Core/src/StreamTracker.cs +++ b/src/SignalR/server/Core/src/StreamTracker.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Channels; using Microsoft.AspNetCore.SignalR.Protocol; @@ -24,8 +26,14 @@ public StreamTracker(int streamBufferCapacity) /// /// Creates a new stream and returns the ChannelReader for it as an object. /// + [UnconditionalSuppressMessage("Trimming", "IL2060:MakeGenericMethod", + Justification = "BuildStream doesn't have trimming annotations.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "HubMethodDescriptor checks for ValueType streaming item types when PublishAot=true. Developers will get an exception in this situation before publishing.")] public object AddStream(string streamId, Type itemType, Type targetType) { + Debug.Assert(RuntimeFeature.IsDynamicCodeSupported || !itemType.IsValueType, "HubMethodDescriptor ensures itemType is not a ValueType when PublishAot=true."); + var newConverter = (IStreamConverter)_buildConverterMethod.MakeGenericMethod(itemType).Invoke(null, _streamConverterArgs)!; _lookup[streamId] = newConverter; return newConverter.GetReaderAsObject(targetType); diff --git a/src/SignalR/server/SignalR/src/Microsoft.AspNetCore.SignalR.csproj b/src/SignalR/server/SignalR/src/Microsoft.AspNetCore.SignalR.csproj index bbe11c2ab0e9..4fabcecbe46b 100644 --- a/src/SignalR/server/SignalR/src/Microsoft.AspNetCore.SignalR.csproj +++ b/src/SignalR/server/SignalR/src/Microsoft.AspNetCore.SignalR.csproj @@ -5,6 +5,7 @@ true false enable + true diff --git a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs index 20a3c6d2ab36..09893e9b02bb 100644 --- a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs @@ -1,7 +1,6 @@ // 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -35,7 +34,6 @@ public static ISignalRServerBuilder AddHubOptions(this ISignalRServerBuild /// /// The to add services to. /// An that can be used to further configure the SignalR services. - [RequiresUnreferencedCode("SignalR does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static ISignalRServerBuilder AddSignalR(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); @@ -55,7 +53,6 @@ public static ISignalRServerBuilder AddSignalR(this IServiceCollection services) /// The to add services to. /// An to configure the provided . /// An that can be used to further configure the SignalR services. - [RequiresUnreferencedCode("SignalR does not currently support trimming or native AOT.", Url = "https://aka.ms/aspnet/trimming")] public static ISignalRServerBuilder AddSignalR(this IServiceCollection services, Action configure) { ArgumentNullException.ThrowIfNull(services); diff --git a/src/SignalR/server/SignalR/test/AddSignalRTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AddSignalRTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/AddSignalRTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AddSignalRTests.cs diff --git a/src/SignalR/server/SignalR/test/AuthConnectionHandler.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AuthConnectionHandler.cs similarity index 100% rename from src/SignalR/server/SignalR/test/AuthConnectionHandler.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AuthConnectionHandler.cs diff --git a/src/SignalR/server/SignalR/test/AuthHub.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AuthHub.cs similarity index 100% rename from src/SignalR/server/SignalR/test/AuthHub.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/AuthHub.cs diff --git a/src/SignalR/server/SignalR/test/CancellationDisposable.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/CancellationDisposable.cs similarity index 100% rename from src/SignalR/server/SignalR/test/CancellationDisposable.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/CancellationDisposable.cs diff --git a/src/SignalR/server/SignalR/test/ClientProxyTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/ClientProxyTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/ClientProxyTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/ClientProxyTests.cs diff --git a/src/SignalR/server/SignalR/test/DefaultHubActivatorTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultHubActivatorTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/DefaultHubActivatorTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultHubActivatorTests.cs diff --git a/src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultHubLifetimeManagerTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/DefaultHubLifetimeManagerTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultHubLifetimeManagerTests.cs diff --git a/src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultTransportFactoryTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultTransportFactoryTests.cs diff --git a/src/SignalR/server/SignalR/test/EchoConnectionHandler.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/EchoConnectionHandler.cs similarity index 100% rename from src/SignalR/server/SignalR/test/EchoConnectionHandler.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/EchoConnectionHandler.cs diff --git a/src/SignalR/server/SignalR/test/EndToEndTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/EndToEndTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/EndToEndTests.cs diff --git a/src/SignalR/server/SignalR/test/HttpHeaderConnectionHandler.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderConnectionHandler.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HttpHeaderConnectionHandler.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HttpHeaderConnectionHandler.cs diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Utils.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Utils.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.Activity.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTests.Activity.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.Activity.cs diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.ClientResult.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTests.ClientResult.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.ClientResult.cs diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs diff --git a/src/SignalR/server/SignalR/test/HubFilterTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubFilterTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubFilterTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubFilterTests.cs diff --git a/src/SignalR/server/SignalR/test/HubReflectionHelperTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubReflectionHelperTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/HubReflectionHelperTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubReflectionHelperTests.cs diff --git a/src/SignalR/server/SignalR/test/Internal/DefaultHubProtocolResolverTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/DefaultHubProtocolResolverTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/Internal/DefaultHubProtocolResolverTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/DefaultHubProtocolResolverTests.cs diff --git a/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/MessageBufferTests.cs diff --git a/src/SignalR/server/SignalR/test/Internal/ReflectionHelperTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/ReflectionHelperTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/Internal/ReflectionHelperTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/ReflectionHelperTests.cs diff --git a/src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/TypedClientBuilderTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/Internal/TypedClientBuilderTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Internal/TypedClientBuilderTests.cs diff --git a/src/SignalR/server/SignalR/test/MapSignalRTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/MapSignalRTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/MapSignalRTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/MapSignalRTests.cs diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj similarity index 100% rename from src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/NativeAotTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/NativeAotTests.cs new file mode 100644 index 000000000000..94f37318eff7 --- /dev/null +++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/NativeAotTests.cs @@ -0,0 +1,340 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Channels; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.SignalR.Tests; + +public partial class NativeAotTests : FunctionalTestBase +{ + [ConditionalFact] + [RemoteExecutionSupported] + public void CanCallAsyncMethods() + { + RunNativeAotTest(static async () => + { + //System.Diagnostics.Debugger.Launch(); + var loggerFactory = new StringLoggerFactory(); + await using (var server = await InProcessTestServer>.StartServer(loggerFactory)) + { + var hubConnectionBuilder = new HubConnectionBuilder() + .WithUrl(server.Url + "/hub"); + AppJsonSerializerContext.AddToJsonHubProtocol(hubConnectionBuilder.Services); + var connection = hubConnectionBuilder.Build(); + + await connection.StartAsync().DefaultTimeout(); + + await connection.InvokeAsync(nameof(AsyncMethodHub.TaskMethod)).DefaultTimeout(); + Assert.Contains("TaskMethod called", loggerFactory.ToString()); + + await connection.InvokeAsync(nameof(AsyncMethodHub.ValueTaskMethod)).DefaultTimeout(); + Assert.Contains("ValueTaskMethod called", loggerFactory.ToString()); + + await connection.InvokeAsync(nameof(AsyncMethodHub.CustomTaskMethod)).DefaultTimeout(); + Assert.Contains("CustomTaskMethod called", loggerFactory.ToString()); + + var result = await connection.InvokeAsync(nameof(AsyncMethodHub.TaskValueMethod)).DefaultTimeout(); + Assert.Equal(42, result); + + result = await connection.InvokeAsync(nameof(AsyncMethodHub.ValueTaskValueMethod)).DefaultTimeout(); + Assert.Equal(43, result); + + result = await connection.InvokeAsync(nameof(AsyncMethodHub.CustomTaskValueMethod)).DefaultTimeout(); + Assert.Equal(44, result); + + var counterResults = new List(); + await foreach (var item in connection.StreamAsync(nameof(AsyncMethodHub.CounterAsyncEnumerable), 4)) + { + counterResults.Add(item); + } + Assert.Equal(["0", "1", "2", "3"], counterResults); + + counterResults.Clear(); + await foreach (var item in connection.StreamAsync(nameof(AsyncMethodHub.CounterAsyncEnumerableImpl), 5)) + { + counterResults.Add(item); + } + Assert.Equal(["0", "1", "2", "3", "4"], counterResults); + + var echoResults = new List(); + var asyncEnumerable = connection.StreamAsync(nameof(AsyncMethodHub.StreamEchoAsyncEnumerable), StreamMessages()); + await foreach (var item in asyncEnumerable) + { + echoResults.Add(item); + } + Assert.Equal(["echo:message one", "echo:message two"], echoResults); + + echoResults.Clear(); + var channel = Channel.CreateBounded(10); + var echoResponseReader = await connection.StreamAsChannelAsync(nameof(AsyncMethodHub.StreamEcho), channel.Reader); + await channel.Writer.WriteAsync("some data"); + await channel.Writer.WriteAsync("some more data"); + await channel.Writer.WriteAsync("even more data"); + channel.Writer.Complete(); + + await foreach (var item in echoResponseReader.ReadAllAsync()) + { + echoResults.Add(item); + } + Assert.Equal(["echo:some data", "echo:some more data", "echo:even more data"], echoResults); + } + }); + } + + private static async IAsyncEnumerable StreamMessages() + { + await Task.Yield(); + yield return "message one"; + await Task.Yield(); + yield return "message two"; + } + + [ConditionalFact] + [RemoteExecutionSupported] + public void UsingValueTypesInStreamingThrows() + { + RunNativeAotTest(static async () => + { + var e = await Assert.ThrowsAsync(() => InProcessTestServer>.StartServer(NullLoggerFactory.Instance)); + Assert.Contains("Unable to stream an item with type 'System.Int32' on method 'Microsoft.AspNetCore.SignalR.Tests.NativeAotTests+AsyncEnumerableIntMethodHub.StreamValueType' because it is a ValueType.", e.Message); + }); + + RunNativeAotTest(static async () => + { + var e = await Assert.ThrowsAsync(() => InProcessTestServer>.StartServer(NullLoggerFactory.Instance)); + Assert.Contains("Unable to stream an item with type 'System.Double' on method 'Microsoft.AspNetCore.SignalR.Tests.NativeAotTests+ChannelDoubleMethodHub.StreamValueType' because it is a ValueType.", e.Message); + }); + } + + private static void RunNativeAotTest(Func test) + { + var options = new RemoteInvokeOptions(); + options.RuntimeConfigurationOptions.Add("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", "false"); + options.RuntimeConfigurationOptions.Add("Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported", "false"); + options.RuntimeConfigurationOptions.Add("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", "false"); + + using var remoteHandle = RemoteExecutor.Invoke(test, options); + } + + public class Startup where THub : Hub + { + public void ConfigureServices(IServiceCollection services) + { + services.AddConnections(); + services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + }); + AppJsonSerializerContext.AddToJsonHubProtocol(services); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHub("/hub"); + }); + } + } + + public class AsyncMethodHub : TestHub + { + public async Task TaskMethod(ILogger logger) + { + await Task.Yield(); // ensure the returned Task gets awaited correctly + logger.LogInformation("TaskMethod called"); + } + + public async ValueTask ValueTaskMethod(ILogger logger) + { + await Task.Yield(); // ensure the returned Task gets awaited correctly + logger.LogInformation("ValueTaskMethod called"); + } + + public TaskDerivedType CustomTaskMethod(ILogger logger) + { + var task = new TaskDerivedType(); + task.Start(); + logger.LogInformation("CustomTaskMethod called"); + return task; + } + + public async Task TaskValueMethod() + { + await Task.Yield(); // ensure the returned Task gets awaited correctly + return 42; + } + + public async ValueTask ValueTaskValueMethod() + { + await Task.Yield(); // ensure the returned Task gets awaited correctly + return 43; + } + + public TaskOfTDerivedType CustomTaskValueMethod() + { + var task = new TaskOfTDerivedType(44); + task.Start(); + return task; + } + + public async IAsyncEnumerable CounterAsyncEnumerable(int count) + { + for (int i = 0; i < count; i++) + { + await Task.Yield(); + yield return i.ToString(CultureInfo.InvariantCulture); + } + } + + public StreamingHub.AsyncEnumerableImpl CounterAsyncEnumerableImpl(int count) + { + return new StreamingHub.AsyncEnumerableImpl(CounterAsyncEnumerable(count)); + } + + public ChannelReader StreamEcho(ChannelReader source) + { + Channel output = Channel.CreateUnbounded(); + + _ = Task.Run(async () => + { + await foreach (var item in source.ReadAllAsync()) + { + await output.Writer.WriteAsync("echo:" + item); + } + + output.Writer.TryComplete(); + }); + + return output.Reader; + } + + public async IAsyncEnumerable StreamEchoAsyncEnumerable(IAsyncEnumerable source) + { + await foreach (var item in source) + { + yield return "echo:" + item; + } + } + } + + public class AsyncEnumerableIntMethodHub : TestHub + { + public async IAsyncEnumerable StreamValueType() + { + await Task.Yield(); + yield return 1; + await Task.Yield(); + yield return 2; + } + } + + public class ChannelDoubleMethodHub : TestHub + { + public async Task StreamValueType(ILogger logger, ChannelReader source) + { + await foreach (var item in source.ReadAllAsync()) + { + logger.LogInformation("Received: {item}", item); + } + } + } + + public class TaskDerivedType : Task + { + public TaskDerivedType() + : base(() => { }) + { + } + } + + public class TaskOfTDerivedType : Task + { + public TaskOfTDerivedType(T input) + : base(() => input) + { + } + } + + public class StringLoggerFactory : ILoggerFactory + { + private readonly StringBuilder _log = new StringBuilder(); + + public void AddProvider(ILoggerProvider provider) { } + + public ILogger CreateLogger(string name) + { + return new StringLogger(name, this); + } + + public void Dispose() { } + + public override string ToString() + { + return _log.ToString(); + } + + private sealed class StringLogger : ILogger + { + private readonly StringLoggerFactory _factory; + private readonly string _name; + + public StringLogger(string name, StringLoggerFactory factory) + { + _name = name; + _factory = factory; + } + + public IDisposable BeginScope(TState state) + { + return new DummyDisposable(); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + string message = string.Format(CultureInfo.InvariantCulture, + "Provider: {0}" + Environment.NewLine + + "Log level: {1}" + Environment.NewLine + + "Event id: {2}" + Environment.NewLine + + "Exception: {3}" + Environment.NewLine + + "Message: {4}", _name, logLevel, eventId, exception?.ToString(), formatter(state, exception)); + _factory._log.AppendLine(message); + } + + private sealed class DummyDisposable : IDisposable + { + public void Dispose() + { + // no-op + } + } + } + } + + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(int))] + internal partial class AppJsonSerializerContext : JsonSerializerContext + { + public static void AddToJsonHubProtocol(IServiceCollection services) + { + services.Configure(o => + { + o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, Default); + }); + } + } +} diff --git a/src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/SerializedHubMessageTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/SerializedHubMessageTests.cs diff --git a/src/SignalR/server/SignalR/test/Startup.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs similarity index 100% rename from src/SignalR/server/SignalR/test/Startup.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Startup.cs diff --git a/src/SignalR/server/SignalR/test/TestAuthHandler.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/TestAuthHandler.cs similarity index 100% rename from src/SignalR/server/SignalR/test/TestAuthHandler.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/TestAuthHandler.cs diff --git a/src/SignalR/server/SignalR/test/TestFilters.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/TestFilters.cs similarity index 100% rename from src/SignalR/server/SignalR/test/TestFilters.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/TestFilters.cs diff --git a/src/SignalR/server/SignalR/test/UncreatableHub.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/UncreatableHub.cs similarity index 100% rename from src/SignalR/server/SignalR/test/UncreatableHub.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/UncreatableHub.cs diff --git a/src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/UserAgentHeaderTest.cs similarity index 100% rename from src/SignalR/server/SignalR/test/UserAgentHeaderTest.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/UserAgentHeaderTest.cs diff --git a/src/SignalR/server/SignalR/test/WebSocketsTransportTests.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs similarity index 100% rename from src/SignalR/server/SignalR/test/WebSocketsTransportTests.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/WebSocketsTransportTests.cs diff --git a/src/SignalR/server/SignalR/test/WriteThenCloseConnectionHandler.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/WriteThenCloseConnectionHandler.cs similarity index 100% rename from src/SignalR/server/SignalR/test/WriteThenCloseConnectionHandler.cs rename to src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/WriteThenCloseConnectionHandler.cs diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj new file mode 100644 index 000000000000..7fabca682f6a --- /dev/null +++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/Microsoft.AspNetCore.SignalR.TrimmingTests.proj @@ -0,0 +1,9 @@ + + + + + Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported + + + + diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/TestTypedClients.cs b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/TestTypedClients.cs new file mode 100644 index 000000000000..41d3a8aea858 --- /dev/null +++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.TrimmingTests/TestTypedClients.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Tests that a SignalR server can use typed clients in a trimmed app. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +var builder = WebApplication.CreateSlimBuilder(args); +builder.Services.AddSignalR(); +AppJsonSerializerContext.AddToJsonHubProtocol(builder.Services); + +var app = builder.Build(); +app.MapHub("/testhub"); +await app.StartAsync().ConfigureAwait(false); + +// connect a client and ensure we can invoke a method on the server +var serverUrl = app.Services.GetRequiredService().Features.Get().Addresses.First(); +var hubConnectionBuilder = new HubConnectionBuilder() + .WithUrl(serverUrl + "/testhub"); +AppJsonSerializerContext.AddToJsonHubProtocol(hubConnectionBuilder.Services); +var connection = hubConnectionBuilder.Build(); + +var receivedMessageTask = new TaskCompletionSource(); +connection.On("ReceiveMessage", (user, message) => +{ + receivedMessageTask.SetResult($"{user}: {message}"); +}); + +await connection.StartAsync().ConfigureAwait(false); + +await connection.InvokeAsync("SendMessage", "userA", "my message").ConfigureAwait(false); + +var receivedMessage = await receivedMessageTask.Task.WaitAsync(TimeSpan.FromSeconds(10)).ConfigureAwait(false); +if (receivedMessage != "userA: my message") +{ + return -1; +} + +return 100; + +public interface ITestHubClientBase +{ + Task BaseMethod(); + Task Unused(string unused); +} + +public interface ITestHubClient : ITestHubClientBase +{ + Task ReceiveMessage(string user, string message); +} + +public class TestHub : Hub +{ + public async Task SendMessage(ILogger logger, string user, string message) + { + logger.LogInformation("Received message from {user}: {message}", user, message); + + await Clients.Caller.BaseMethod().ConfigureAwait(false); + + await Clients.All.ReceiveMessage(user, message).ConfigureAwait(false); + } +} + +[JsonSerializable(typeof(string))] +internal sealed partial class AppJsonSerializerContext : JsonSerializerContext +{ + public static void AddToJsonHubProtocol(IServiceCollection services) + { + services.Configure(o => + { + o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, Default); + }); + } +}