فهرست منبع

Strongly-typed SignalR hub proxies and callback handlers (#33717)

Mehmet Akbulut 4 سال پیش
والد
کامیت
c9dda658d0
18فایلهای تغییر یافته به همراه2176 افزوده شده و 1 حذف شده
  1. 18 0
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 2 1
      src/SignalR/SignalR.slnf
  4. 176 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/DiagnosticDescriptors.cs
  5. 31 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/GeneratorHelpers.cs
  6. 208 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.Emitter.cs
  7. 333 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.Parser.cs
  8. 52 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.SourceGenerationSpec.cs
  9. 45 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.cs
  10. 205 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.Emitter.cs
  11. 345 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.Parser.cs
  12. 68 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.SourceGenerationSpec.cs
  13. 45 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.cs
  14. 19 0
      src/SignalR/clients/csharp/Client.SourceGenerator/src/Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj
  15. 263 0
      src/SignalR/clients/csharp/Client/test/UnitTests/HubClientProxyGeneratorTests.cs
  16. 337 0
      src/SignalR/clients/csharp/Client/test/UnitTests/HubServerProxyGeneratorTests.cs
  17. 2 0
      src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj
  18. 26 0
      src/SignalR/clients/csharp/Client/test/UnitTests/MockHubConnection.cs

+ 18 - 0
AspNetCore.sln

@@ -1634,6 +1634,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.R
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging.W3C.Sample", "src\Middleware\HttpLogging\samples\Logging.W3C.Sample\Logging.W3C.Sample.csproj", "{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client.SourceGenerator", "Client.SourceGenerator", "{4FC50620-4C8B-495F-859C-BFACAD158033}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SignalR.Client.SourceGenerator", "src\SignalR\clients\csharp\Client.SourceGenerator\src\Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj", "{E090F82D-8345-477E-92E8-F724F08ADC56}"
+EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Internal.SourceGenerator.Transport", "src\Razor\Microsoft.AspNetCore.Razor.Internal.SourceGenerator.Transport\Microsoft.AspNetCore.Razor.Internal.SourceGenerator.Transport.csproj", "{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpClientApp", "src\Servers\Kestrel\samples\HttpClientApp\HttpClientApp.csproj", "{514726D2-3D2E-44C1-B056-163E37DE3E8B}"
@@ -9912,6 +9916,18 @@ Global
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Release|x64.Build.0 = Release|Any CPU
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Release|x86.ActiveCfg = Release|Any CPU
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Release|x86.Build.0 = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|x64.Build.0 = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Debug|x86.Build.0 = Debug|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|x64.ActiveCfg = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|x64.Build.0 = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|x86.ActiveCfg = Release|Any CPU
+		{E090F82D-8345-477E-92E8-F724F08ADC56}.Release|x86.Build.0 = Release|Any CPU
 		{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -11185,6 +11201,8 @@ Global
 		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9}
 		{F599EAA6-399F-4A91-9B1F-D311305B43D9} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9}
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85} = {022B4B80-E813-4256-8034-11A68146F4EF}
+		{4FC50620-4C8B-495F-859C-BFACAD158033} = {2637A22F-182F-4659-A394-C9BC1514B321}
+		{E090F82D-8345-477E-92E8-F724F08ADC56} = {4FC50620-4C8B-495F-859C-BFACAD158033}
 		{247E7B6F-FBA2-41A9-BA03-C7C4DF28091C} = {B27FBAC2-ADA3-4A05-B232-64011B6B2DA3}
 		{514726D2-3D2E-44C1-B056-163E37DE3E8B} = {7B976D8F-EA31-4C0B-97BD-DFD9B3CC86FB}
 		{48526D13-69E2-4409-A57B-C3FA3C64B4F7} = {9F21A235-436E-4020-A076-1DF4F89D0CA0}

+ 1 - 0
eng/ProjectReferences.props

@@ -128,6 +128,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.AzureAppServices.HostingStartup" ProjectPath="$(RepoRoot)src\Azure\AzureAppServices.HostingStartup\src\Microsoft.AspNetCore.AzureAppServices.HostingStartup.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.AzureAppServicesIntegration" ProjectPath="$(RepoRoot)src\Azure\AzureAppServicesIntegration\src\Microsoft.AspNetCore.AzureAppServicesIntegration.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.SignalR.Client.Core" ProjectPath="$(RepoRoot)src\SignalR\clients\csharp\Client.Core\src\Microsoft.AspNetCore.SignalR.Client.Core.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.SignalR.Client.SourceGenerator" ProjectPath="$(RepoRoot)src\SignalR\clients\csharp\Client.SourceGenerator\src\Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.SignalR.Client" ProjectPath="$(RepoRoot)src\SignalR\clients\csharp\Client\src\Microsoft.AspNetCore.SignalR.Client.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Connections.Client" ProjectPath="$(RepoRoot)src\SignalR\clients\csharp\Http.Connections.Client\src\Microsoft.AspNetCore.Http.Connections.Client.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Connections.Common" ProjectPath="$(RepoRoot)src\SignalR\common\Http.Connections.Common\src\Microsoft.AspNetCore.Http.Connections.Common.csproj" />

+ 2 - 1
src/SignalR/SignalR.slnf

@@ -40,6 +40,7 @@
       "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
       "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
       "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
+      "src\\SignalR\\clients\\csharp\\Client.SourceGenerator\\src\\Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj",
       "src\\SignalR\\clients\\csharp\\Client.Core\\src\\Microsoft.AspNetCore.SignalR.Client.Core.csproj",
       "src\\SignalR\\clients\\csharp\\Client\\src\\Microsoft.AspNetCore.SignalR.Client.csproj",
       "src\\SignalR\\clients\\csharp\\Client\\test\\FunctionalTests\\Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj",
@@ -72,4 +73,4 @@
       "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
-}
+}

+ 176 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/DiagnosticDescriptors.cs

@@ -0,0 +1,176 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal static class DiagnosticDescriptors
+    {
+        // Ranges
+        // SSG0000-0099: HubServerProxyGenerator
+        // SSG0100-0199: HubClientProxyGenerator
+
+        public static DiagnosticDescriptor HubServerProxyNonInterfaceGenericTypeArgument { get; } = new DiagnosticDescriptor(
+            id: "SSG0000",
+            title: "Non-interface generic type argument",
+            messageFormat: "Only interfaces are accepted. '{0}' is not an interface.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyUnsupportedReturnType { get; } = new DiagnosticDescriptor(
+            id: "SSG0001",
+            title: "Unsupported return type",
+            messageFormat: "'{0}' has a return type of '{1}' but only Task, ValueTask, Task<T> and ValueTask<T> are supported for source generation.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Warning,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor TooManyHubServerProxyAttributedMethods { get; } = new DiagnosticDescriptor(
+            id: "SSG0002",
+            title: "Too many HubServerProxy attributed methods",
+            messageFormat: "There can only be one HubServerProxy attributed method.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodBadAccessibility { get; } = new DiagnosticDescriptor(
+            id: "SSG0003",
+            title: "HubServerProxy attributed method has bad accessibility",
+            messageFormat: "HubServerProxy attributed method may only have an accessibility of public, internal, protected, protected internal or private.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodIsNotPartial { get; } = new DiagnosticDescriptor(
+            id: "SSG0004",
+            title: "HubServerProxy attributed method is not partial",
+            messageFormat: "HubServerProxy attributed method must be partial.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodIsNotExtension { get; } = new DiagnosticDescriptor(
+            id: "SSG0005",
+            title: "HubServerProxy attributed method is not an extension method",
+            messageFormat: "HubServerProxy attributed method must be an extension method for HubConnection.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodTypeArgCountIsBad { get; } = new DiagnosticDescriptor(
+            id: "SSG0006",
+            title: "HubServerProxy attributed method has bad number of type arguments",
+            messageFormat: "HubServerProxy attributed method must have exactly one type argument.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodTypeArgAndReturnTypeDoesNotMatch { get; } = new DiagnosticDescriptor(
+            id: "SSG0007",
+            title: "HubServerProxy attributed method type argument and return type does not match",
+            messageFormat: "HubServerProxy attributed method must have the same type argument and return type.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodArgCountIsBad { get; } = new DiagnosticDescriptor(
+            id: "SSG0008",
+            title: "HubServerProxy attributed method has bad number of arguments",
+            messageFormat: "HubServerProxy attributed method must have exactly one argument which must be of type HubConnection.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubServerProxyAttributedMethodArgIsNotHubConnection { get; } = new DiagnosticDescriptor(
+            id: "SSG0009",
+            title: "HubServerProxy attributed method has argument of wrong type",
+            messageFormat: "HubServerProxy attributed method must have exactly one argument which must be of type HubConnection.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        // HubClientProxy section
+        
+        public static DiagnosticDescriptor HubClientProxyUnsupportedReturnType { get; } = new DiagnosticDescriptor(
+            id: "SSG0100",
+            title: "Unsupported return type",
+            messageFormat: "'{0}' has a return type of '{1}' but only void and Task are supported for callback methods.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Warning,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor TooManyHubClientProxyAttributedMethods { get; } = new DiagnosticDescriptor(
+            id: "SSG0102",
+            title: "Too many HubClientProxy attributed methods",
+            messageFormat: "There can only be one HubClientProxy attributed method.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodBadAccessibility { get; } = new DiagnosticDescriptor(
+            id: "SSG0103",
+            title: "HubClientProxy attributed method has bad accessibility",
+            messageFormat: "HubClientProxy attributed method may only have an accessibility of public, internal, protected, protected internal or private.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodIsNotPartial { get; } = new DiagnosticDescriptor(
+            id: "SSG0104",
+            title: "HubClientProxy attributed method is not partial",
+            messageFormat: "HubClientProxy attributed method must be partial.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodIsNotExtension { get; } = new DiagnosticDescriptor(
+            id: "SSG0105",
+            title: "HubClientProxy attributed method is not an extension method",
+            messageFormat: "HubClientProxy attributed method must be an extension method for HubConnection.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodTypeArgCountIsBad { get; } = new DiagnosticDescriptor(
+            id: "SSG0106",
+            title: "HubClientProxy attributed method has bad number of type arguments",
+            messageFormat: "HubClientProxy attributed method must have exactly one type argument.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodTypeArgAndProviderTypeDoesNotMatch { get; } = new DiagnosticDescriptor(
+            id: "SSG0107",
+            title: "HubClientProxy attributed method type argument and return type does not match",
+            messageFormat: "HubClientProxy attributed method must have the same type argument and return type.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodArgCountIsBad { get; } = new DiagnosticDescriptor(
+            id: "SSG0108",
+            title: "HubClientProxy attributed method has bad number of arguments",
+            messageFormat: "HubClientProxy attributed method must have exactly two arguments.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodArgIsNotHubConnection { get; } = new DiagnosticDescriptor(
+            id: "SSG0109",
+            title: "HubClientProxy attributed method has first argument of wrong type",
+            messageFormat: "HubClientProxy attributed method must have its first argument type be HubConnection.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+
+        public static DiagnosticDescriptor HubClientProxyAttributedMethodHasBadReturnType { get; } = new DiagnosticDescriptor(
+            id: "SSG0110",
+            title: "HubClientProxy attributed method has wrong return type",
+            messageFormat: "HubClientProxy attributed method must have a return type of IDisposable.",
+            category: "SignalR.Client.SourceGenerator",
+            defaultSeverity: DiagnosticSeverity.Error,
+            isEnabledByDefault: true);
+    }
+}

+ 31 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/GeneratorHelpers.cs

@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal static class GeneratorHelpers
+    {
+        public static string GetAccessibilityString(Accessibility accessibility)
+        {
+            switch (accessibility)
+            {
+                case Accessibility.Private:
+                    return "private";
+                case Accessibility.ProtectedAndInternal:
+                    return "protected internal";
+                case Accessibility.Protected:
+                    return "protected";
+                case Accessibility.Internal:
+                    return "internal";
+                case Accessibility.Public:
+                    return "public";
+                default:
+                    return null;
+            }
+        }
+    }
+}

+ 208 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.Emitter.cs

@@ -0,0 +1,208 @@
+// 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.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubClientProxyGenerator
+    {
+        public class Emitter
+        {
+            private readonly SourceProductionContext _context;
+            private readonly SourceGenerationSpec _spec;
+
+            public Emitter(SourceProductionContext context, SourceGenerationSpec spec)
+            {
+                _context = context;
+                _spec = spec;
+            }
+
+            public void Emit()
+            {
+                if (string.IsNullOrEmpty(_spec.SetterClassAccessibility) ||
+                    string.IsNullOrEmpty(_spec.SetterMethodAccessibility) ||
+                    string.IsNullOrEmpty(_spec.SetterClassName) ||
+                    string.IsNullOrEmpty(_spec.SetterMethodName) ||
+                    string.IsNullOrEmpty(_spec.SetterTypeParameterName) ||
+                    string.IsNullOrEmpty(_spec.SetterHubConnectionParameterName))
+                {
+                    return;
+                }
+
+                // Generate extensions and other user facing mostly-static code in a single source file
+                EmitExtensions();
+                // Generate specific callback registration methods in their own source file for each provider type
+                foreach (var typeSpec in _spec.Types)
+                {
+                    EmitRegistrationMethod(typeSpec);
+                }
+            }
+
+            private void EmitExtensions()
+            {
+                var registerProviderBody = new StringBuilder();
+
+                // Generate body of RegisterCallbackProvider<T>
+                foreach (var typeSpec in _spec.Types)
+                {
+                    var methodName = $"Register{typeSpec.FullyQualifiedTypeName.Replace(".", string.Empty)}";
+                    var fqtn = typeSpec.FullyQualifiedTypeName;
+                    registerProviderBody.AppendLine($@"
+            if (typeof({_spec.SetterTypeParameterName}) == typeof({fqtn}))
+            {{
+                return (System.IDisposable) new CallbackProviderRegistration({methodName}({_spec.SetterHubConnectionParameterName}, ({fqtn}) provider));
+            }}");
+                }
+
+                // Generate RegisterCallbackProvider<T> extension method and CallbackProviderRegistration class
+                // RegisterCallbackProvider<T> is used by end-user to register their callback provider types
+                // CallbackProviderRegistration is a private implementation of IDisposable which simply holds
+                //  an array of IDisposables acquired from registration of each callback method from HubConnection
+                var extensions = $@"// <auto-generated>
+// Generated by Microsoft.AspNetCore.Client.SourceGenerator
+// </auto-generated>
+
+#nullable enable
+
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace {_spec.SetterNamespace}
+{{
+    {_spec.SetterClassAccessibility} static partial class {_spec.SetterClassName}
+    {{
+        {_spec.SetterMethodAccessibility} static partial System.IDisposable {_spec.SetterMethodName}<{_spec.SetterTypeParameterName}>(this HubConnection {_spec.SetterHubConnectionParameterName}, {_spec.SetterTypeParameterName} {_spec.SetterProviderParameterName})
+        {{
+            if ({_spec.SetterProviderParameterName} is null)
+            {{
+                throw new System.ArgumentNullException(""{_spec.SetterProviderParameterName}"");
+            }}
+{registerProviderBody.ToString()}
+            throw new System.ArgumentException(nameof({_spec.SetterTypeParameterName}));
+        }}
+
+        private sealed class CallbackProviderRegistration : System.IDisposable
+        {{
+            private System.IDisposable[]? registrations;
+            public CallbackProviderRegistration(params System.IDisposable[] registrations)
+            {{
+                this.registrations = registrations;
+            }}
+
+            public void Dispose()
+            {{
+                if (this.registrations is null)
+                {{
+                    return;
+                }}
+
+                System.Collections.Generic.List<System.Exception>? exceptions = null;
+                foreach(var registration in this.registrations)
+                {{
+                    try
+                    {{
+                        registration.Dispose();
+                    }}
+                    catch (System.Exception exc)
+                    {{
+                        if (exceptions is null)
+                        {{
+                            exceptions = new ();
+                        }}
+
+                        exceptions.Add(exc);
+                    }}
+                }}
+                this.registrations = null;
+                if (exceptions is not null)
+                {{
+                    throw new System.AggregateException(exceptions);
+                }}
+            }}
+        }}
+    }}
+}}";
+
+                _context.AddSource("HubClientProxy.g.cs", SourceText.From(extensions.ToString(), Encoding.UTF8));
+            }
+
+            private void EmitRegistrationMethod(TypeSpec typeSpec)
+            {
+                // The actual registration method goes thru each method that the callback provider type has and then
+                //  registers the method with HubConnection and stashes the returned IDisposable into an array for
+                //  later consumption by CallbackProviderRegistration's constructor
+                var registrationMethodBody = new StringBuilder($@"// <auto-generated>
+// Generated by Microsoft.AspNetCore.Client.SourceGenerator
+// </auto-generated>
+
+#nullable enable
+
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace {_spec.SetterNamespace}
+{{
+    {_spec.SetterClassAccessibility} static partial class {_spec.SetterClassName}
+    {{
+        private static System.IDisposable[] Register{typeSpec.FullyQualifiedTypeName.Replace(".", string.Empty)}(HubConnection connection, {typeSpec.FullyQualifiedTypeName} provider)
+        {{
+            var registrations = new System.IDisposable[{typeSpec.Methods.Count}];");
+
+                // Generate each of the methods
+                var i = 0;
+                foreach (var member in typeSpec.Methods)
+                {
+                    var genericArgs = new StringBuilder();
+                    var lambaParams = new StringBuilder();
+
+                    // Populate call with its parameters
+                    var first = true;
+                    foreach (var parameter in member.Arguments)
+                    {
+                        if (first)
+                        {
+                            genericArgs.Append('<');
+                            lambaParams.Append('(');
+                        }
+                        else
+                        {
+                            genericArgs.Append(", ");
+                            lambaParams.Append(", ");
+                        }
+
+                        first = false;
+                        genericArgs.Append($"{parameter.FullyQualifiedTypeName}");
+                        lambaParams.Append($"{parameter.Name}");
+                    }
+
+                    if (!first)
+                    {
+                        genericArgs.Append('>');
+                        lambaParams.Append(')');
+                    }
+                    else
+                    {
+                        lambaParams.Append("()");
+                    }
+
+
+                    var lambda = $"{lambaParams} => provider.{member.Name}{lambaParams}";
+                    var call = $"connection.On{genericArgs}(\"{member.Name}\", {lambda})";
+
+                    registrationMethodBody.AppendLine($@"
+            registrations[{i}] = {call};");
+                    ++i;
+                }
+                registrationMethodBody.AppendLine(@"
+            return registrations;
+        }
+    }
+}");
+
+                _context.AddSource($"HubClientProxy.{typeSpec.TypeName}.g.cs", SourceText.From(registrationMethodBody.ToString(), Encoding.UTF8));
+            }
+        }
+    }
+}

+ 333 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.Parser.cs

@@ -0,0 +1,333 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubClientProxyGenerator
+    {
+        public class Parser
+        {
+            internal static bool IsSyntaxTargetForAttribute(SyntaxNode node) => node is AttributeSyntax
+            {
+                Name: IdentifierNameSyntax
+                {
+                    Identifier:
+                    {
+                        Text: "HubClientProxy"
+                    }
+                },
+                Parent:
+                {
+                    Parent: MethodDeclarationSyntax
+                    {
+                        Parent: ClassDeclarationSyntax
+                    }
+                }
+            };
+
+            internal static MethodDeclarationSyntax? GetSemanticTargetForAttribute(GeneratorSyntaxContext context)
+            {
+                var attributeSyntax = (AttributeSyntax)context.Node;
+                var attributeSymbol = ModelExtensions.GetSymbolInfo(context.SemanticModel, attributeSyntax).Symbol;
+
+                if (attributeSymbol is null ||
+                    !attributeSymbol.ToString().EndsWith("HubClientProxyAttribute()", StringComparison.Ordinal))
+                {
+                    return null;
+                }
+
+                return (MethodDeclarationSyntax)attributeSyntax.Parent.Parent;
+            }
+
+            private static bool IsExtensionMethodSignatureValid(IMethodSymbol symbol, SourceProductionContext context)
+            {
+                // Check that the method is partial
+                if (!symbol.IsPartialDefinition)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodIsNotPartial,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method is an extension
+                if (!symbol.IsExtensionMethod)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodIsNotExtension,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method has one type parameter
+                if (symbol.Arity != 1)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodTypeArgCountIsBad,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method has correct parameters
+                if (symbol.Parameters.Length != 2)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodArgCountIsBad,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the type parameter matches 2nd parameter type
+                if (!SymbolEqualityComparer.Default.Equals(symbol.TypeArguments[0], symbol.Parameters[1].Type))
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodTypeArgAndProviderTypeDoesNotMatch,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the type parameter matches 2nd parameter type
+                if (symbol.ReturnType.ToString() != "System.IDisposable")
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodHasBadReturnType,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                var hubConnectionSymbol = symbol.Parameters[0].Type as INamedTypeSymbol;
+                if (hubConnectionSymbol.ToString() != "Microsoft.AspNetCore.SignalR.Client.HubConnection")
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodArgIsNotHubConnection,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                return true;
+            }
+
+            private static bool IsExtensionClassSignatureValid(ClassDeclarationSyntax syntax, SourceProductionContext context)
+            {
+                // Check partialness
+                var hasPartialModifier = false;
+                foreach (var modifier in syntax.Modifiers)
+                {
+                    if (modifier.Kind() == SyntaxKind.PartialKeyword)
+                    {
+                        hasPartialModifier = true;
+                    }
+                }
+                if (!hasPartialModifier)
+                {
+                    return false;
+                }
+
+                return true;
+            }
+
+            internal static bool IsSyntaxTargetForGeneration(SyntaxNode node) => node is MemberAccessExpressionSyntax
+            {
+                Name: GenericNameSyntax
+                {
+                    Arity: 1
+                }
+            };
+
+            internal static MemberAccessExpressionSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
+            {
+                var memberAccessExpressionSyntax = (MemberAccessExpressionSyntax)context.Node;
+
+                if (ModelExtensions.GetSymbolInfo(context.SemanticModel, memberAccessExpressionSyntax).Symbol is not IMethodSymbol
+                    methodSymbol)
+                {
+                    return null;
+                }
+
+                if (!methodSymbol.IsExtensionMethod)
+                {
+                    return null;
+                }
+
+                foreach (var attributeData in methodSymbol.GetAttributes())
+                {
+                    if (!attributeData.AttributeClass.ToString()
+                        .EndsWith("HubClientProxyAttribute", StringComparison.Ordinal))
+                    {
+                        continue;
+                    }
+
+                    return memberAccessExpressionSyntax;
+                }
+
+                return null;
+            }
+
+            private readonly SourceProductionContext _context;
+            private readonly Compilation _compilation;
+
+            public Parser(SourceProductionContext context, Compilation compilation)
+            {
+                _context = context;
+                _compilation = compilation;
+            }
+
+            internal SourceGenerationSpec Parse(ImmutableArray<MethodDeclarationSyntax> methodDeclarationSyntaxes, ImmutableArray<MemberAccessExpressionSyntax> syntaxList)
+            {
+                // Source generation spec will be populated by type specs for each hub type.
+                // Type specs themselves are populated by method specs which are populated by argument specs.
+                // Source generation spec is then used by emitter to actually generate source.
+                var sourceGenerationSpec = new SourceGenerationSpec();
+
+                // There must be exactly one attributed method
+                if (methodDeclarationSyntaxes.Length != 1)
+                {
+                    // Report diagnostic for each attributed method when there are many
+                    foreach (var extraneous in methodDeclarationSyntaxes)
+                    {
+                        _context.ReportDiagnostic(
+                            Diagnostic.Create(
+                                DiagnosticDescriptors.TooManyHubClientProxyAttributedMethods,
+                                extraneous.GetLocation()));
+                    }
+
+                    // nothing to do
+                    return sourceGenerationSpec;
+                }
+
+                var methodDeclarationSyntax = methodDeclarationSyntaxes[0];
+
+                var registerCallbackProviderSemanticModel = _compilation.GetSemanticModel(methodDeclarationSyntax.SyntaxTree);
+                var registerCallbackProviderMethodSymbol = (IMethodSymbol)registerCallbackProviderSemanticModel.GetDeclaredSymbol(methodDeclarationSyntax);
+                var registerCallbackProviderClassSymbol = (INamedTypeSymbol)registerCallbackProviderMethodSymbol.ContainingSymbol;
+
+                // Populate spec with metadata on user-specific get proxy method and class
+                if (!IsExtensionMethodSignatureValid(registerCallbackProviderMethodSymbol, _context))
+                {
+                    return sourceGenerationSpec;
+                }
+                if (!IsExtensionClassSignatureValid((ClassDeclarationSyntax)methodDeclarationSyntax.Parent, _context))
+                {
+                    return sourceGenerationSpec;
+                }
+
+                sourceGenerationSpec.SetterMethodAccessibility =
+                    GeneratorHelpers.GetAccessibilityString(registerCallbackProviderMethodSymbol.DeclaredAccessibility);
+                sourceGenerationSpec.SetterClassAccessibility =
+                    GeneratorHelpers.GetAccessibilityString(registerCallbackProviderClassSymbol.DeclaredAccessibility);
+                if (sourceGenerationSpec.SetterMethodAccessibility is null)
+                {
+                    _context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubClientProxyAttributedMethodBadAccessibility,
+                        methodDeclarationSyntax.GetLocation()));
+                    return sourceGenerationSpec;
+                }
+                sourceGenerationSpec.SetterMethodName = registerCallbackProviderMethodSymbol.Name;
+                sourceGenerationSpec.SetterClassName = registerCallbackProviderClassSymbol.Name;
+                sourceGenerationSpec.SetterNamespace = registerCallbackProviderClassSymbol.ContainingNamespace.ToString();
+                sourceGenerationSpec.SetterTypeParameterName = registerCallbackProviderMethodSymbol.TypeParameters[0].Name;
+                sourceGenerationSpec.SetterHubConnectionParameterName = registerCallbackProviderMethodSymbol.Parameters[0].Name;
+                sourceGenerationSpec.SetterProviderParameterName = registerCallbackProviderMethodSymbol.Parameters[1].Name;
+
+                var providerSymbols = new Dictionary<string, (ITypeSymbol, MemberAccessExpressionSyntax)>();
+
+                // Go thru candidates and filter further
+                foreach (var memberAccess in syntaxList)
+                {
+                    // Extract type symbol
+                    ITypeSymbol symbol;
+                    if (memberAccess.Name is GenericNameSyntax { Arity: 1 } gns)
+                    {
+                        // Method is using generic syntax so the sole generic arg is the type
+                        var argType = gns.TypeArgumentList.Arguments[0];
+                        var argModel = _compilation.GetSemanticModel(argType.SyntaxTree);
+                        symbol = (ITypeSymbol)argModel.GetSymbolInfo(argType).Symbol;
+                    }
+                    else if (memberAccess.Name is not GenericNameSyntax
+                             && memberAccess.Parent.ChildNodes().FirstOrDefault(x => x is ArgumentListSyntax) is
+                                 ArgumentListSyntax
+                             { Arguments: { Count: 1 } } als)
+                    {
+                        // Method isn't using generic syntax so inspect first expression in arguments to deduce the type
+                        var argModel = _compilation.GetSemanticModel(als.Arguments[0].Expression.SyntaxTree);
+                        var argTypeInfo = argModel.GetTypeInfo(als.Arguments[0].Expression);
+                        symbol = argTypeInfo.Type;
+                    }
+                    else
+                    {
+                        // If we are here then candidate has different number of args than we expect so we skip
+                        continue;
+                    }
+
+                    // Receiver is a HubConnection, so save argument symbol for generation
+                    providerSymbols[symbol.Name] = (symbol, memberAccess);
+                }
+
+                // Generate spec for each provider
+                foreach (var (providerSymbol, memberAccess) in providerSymbols.Values)
+                {
+                    var typeSpec = new TypeSpec
+                    {
+                        FullyQualifiedTypeName = providerSymbol.ToString(),
+                        TypeName = providerSymbol.Name,
+                        CallSite = memberAccess.GetLocation()
+                    };
+
+                    var members = providerSymbol.GetMembers()
+                        .Where(member => member.Kind == SymbolKind.Method)
+                        .Select(member => (IMethodSymbol)member)
+                        .Union<IMethodSymbol>(providerSymbol.AllInterfaces.SelectMany(x => x
+                            .GetMembers()
+                            .Where(member => member.Kind == SymbolKind.Method)
+                            .Select(member => (IMethodSymbol)member)), SymbolEqualityComparer.Default).ToList();
+
+                    // Generate spec for each method
+                    foreach (var member in members)
+                    {
+                        var methodSpec = new MethodSpec
+                        {
+                            Name = member.Name
+                        };
+
+                        // Validate return type
+                        if (!(member.ReturnsVoid || member.ReturnType is INamedTypeSymbol { Arity: 0, Name: "Task" }))
+                        {
+                            _context.ReportDiagnostic(Diagnostic.Create(
+                                DiagnosticDescriptors.HubClientProxyUnsupportedReturnType,
+                                typeSpec.CallSite,
+                                methodSpec.Name, member.ReturnType.Name));
+                            methodSpec.Support = SupportClassification.UnsupportedReturnType;
+                            methodSpec.SupportHint = "Return type must be void or Task";
+                        }
+
+                        // Generate spec for each argument
+                        foreach (var parameter in member.Parameters)
+                        {
+                            var argumentSpec = new ArgumentSpec
+                            {
+                                Name = parameter.Name,
+                                FullyQualifiedTypeName = parameter.Type.ToString()
+                            };
+
+                            methodSpec.Arguments.Add(argumentSpec);
+                        }
+
+                        typeSpec.Methods.Add(methodSpec);
+                    }
+
+                    sourceGenerationSpec.Types.Add(typeSpec);
+                }
+
+                return sourceGenerationSpec;
+            }
+        }
+    }
+}

+ 52 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.SourceGenerationSpec.cs

@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubClientProxyGenerator
+    {
+        public class SourceGenerationSpec
+        {
+            public string? SetterNamespace;
+            public string? SetterClassName;
+            public string? SetterMethodName;
+            public string? SetterTypeParameterName;
+            public string? SetterHubConnectionParameterName;
+            public string? SetterProviderParameterName;
+            public string? SetterMethodAccessibility;
+            public string? SetterClassAccessibility;
+            public List<TypeSpec> Types = new();
+        }
+
+        public class TypeSpec
+        {
+            public string TypeName;
+            public List<MethodSpec> Methods = new();
+            public Location CallSite;
+            public string FullyQualifiedTypeName;
+        }
+
+        public class MethodSpec
+        {
+            public string Name;
+            public List<ArgumentSpec> Arguments = new();
+            public SupportClassification Support;
+            public string? SupportHint;
+        }
+
+        public enum SupportClassification
+        {
+            Supported,
+            UnsupportedReturnType
+        }
+
+        public class ArgumentSpec
+        {
+            public string Name;
+            public string FullyQualifiedTypeName;
+        }
+    }
+}

+ 45 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubClientProxyGenerator.cs

@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    [Generator]
+    internal partial class HubClientProxyGenerator : IIncrementalGenerator
+    {
+        public void Initialize(IncrementalGeneratorInitializationContext context)
+        {
+            var methodDeclaration = context.SyntaxProvider
+                .CreateSyntaxProvider(static (s, _) => Parser.IsSyntaxTargetForAttribute(s),
+                    static (ctx, _) => Parser.GetSemanticTargetForAttribute(ctx))
+                .Where(static m => m is not null)
+                .Collect();
+
+            var memberAccessExpressions = context.SyntaxProvider
+                .CreateSyntaxProvider(static (s, _) => Parser.IsSyntaxTargetForGeneration(s),
+                    static (ctx, _) => Parser.GetSemanticTargetForGeneration(ctx))
+                .Where(static m => m is not null);
+
+            var compilationAndMethodDeclaration = context.CompilationProvider.Combine(methodDeclaration);
+
+            var payload = compilationAndMethodDeclaration
+                .Combine(memberAccessExpressions.Collect());
+
+            context.RegisterSourceOutput(payload, static (spc, source) =>
+                Execute(source.Left.Left, source.Left.Right, source.Right, spc));
+        }
+
+        private static void Execute(Compilation compilation, ImmutableArray<MethodDeclarationSyntax> methodDeclarationSyntaxes, ImmutableArray<MemberAccessExpressionSyntax> memberAccessExpressionSyntaxes, SourceProductionContext context)
+        {
+            var parser = new Parser(context, compilation);
+            var spec = parser.Parse(methodDeclarationSyntaxes, memberAccessExpressionSyntaxes);
+            var emitter = new Emitter(context, spec);
+            emitter.Emit();
+        }
+    }
+}

+ 205 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.Emitter.cs

@@ -0,0 +1,205 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubServerProxyGenerator
+    {
+        public class Emitter
+        {
+            private readonly SourceProductionContext _context;
+            private readonly SourceGenerationSpec _spec;
+
+            public Emitter(SourceProductionContext context, SourceGenerationSpec spec)
+            {
+                _context = context;
+                _spec = spec;
+            }
+
+            public void Emit()
+            {
+                if (string.IsNullOrEmpty(_spec.GetterClassAccessibility) ||
+                    string.IsNullOrEmpty(_spec.GetterMethodAccessibility) ||
+                    string.IsNullOrEmpty(_spec.GetterClassName) ||
+                    string.IsNullOrEmpty(_spec.GetterMethodName) ||
+                    string.IsNullOrEmpty(_spec.GetterTypeParameterName) ||
+                    string.IsNullOrEmpty(_spec.GetterHubConnectionParameterName))
+                {
+                    return;
+                }
+
+                // Generate extensions and other user facing mostly-static code in a single source file
+                EmitExtensions();
+                // Generate hub proxy code in its own source file for each hub proxy type
+                foreach (var classSpec in _spec.Classes)
+                {
+                    EmitProxy(classSpec);
+                }
+            }
+
+            private void EmitExtensions()
+            {
+                var getProxyBody = new StringBuilder();
+
+                foreach (var classSpec in _spec.Classes)
+                {
+                    var fqIntfTypeName = classSpec.FullyQualifiedInterfaceTypeName;
+                    var fqClassTypeName =
+                        $"{_spec.GetterNamespace}.{_spec.GetterClassName}.{classSpec.ClassTypeName}";
+                    getProxyBody.Append($@"
+            if (typeof({_spec.GetterTypeParameterName}) == typeof({fqIntfTypeName}))
+            {{
+                return ({_spec.GetterTypeParameterName}) ({fqIntfTypeName}) new {fqClassTypeName}({_spec.GetterHubConnectionParameterName});
+            }}");
+                }
+
+                var getProxy = $@"// <auto-generated>
+// Generated by Microsoft.AspNetCore.Client.SourceGenerator
+// </auto-generated>
+
+#nullable enable
+
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace {_spec.GetterNamespace}
+{{
+    {_spec.GetterClassAccessibility} static partial class {_spec.GetterClassName}
+    {{
+        {_spec.GetterMethodAccessibility} static partial {_spec.GetterTypeParameterName} {_spec.GetterMethodName}<{_spec.GetterTypeParameterName}>(this HubConnection {_spec.GetterHubConnectionParameterName})
+        {{
+{getProxyBody.ToString()}
+            throw new System.ArgumentException(nameof({_spec.GetterTypeParameterName}));
+        }}
+    }}
+}}";
+
+                _context.AddSource("HubServerProxy.g.cs", SourceText.From(getProxy.ToString(), Encoding.UTF8));
+            }
+
+            private void EmitProxy(ClassSpec classSpec)
+            {
+                var methods = new StringBuilder();
+
+                foreach (var methodSpec in classSpec.Methods)
+                {
+                    var signature = new StringBuilder($"public {methodSpec.FullyQualifiedReturnTypeName} {methodSpec.Name}(");
+                    var callArgs = new StringBuilder("");
+                    var signatureArgs = new StringBuilder("");
+                    var first = true;
+                    foreach (var argumentSpec in methodSpec.Arguments)
+                    {
+                        if (!first)
+                        {
+                            signatureArgs.Append(", ");
+                        }
+
+                        first = false;
+                        signatureArgs.Append($"{argumentSpec.FullyQualifiedTypeName} {argumentSpec.Name}");
+                        callArgs.Append($", {argumentSpec.Name}");
+                    }
+                    signature.Append(signatureArgs);
+                    signature.Append(')');
+
+                    // Prepare method body
+                    var body = "";
+                    if (methodSpec.Support != SupportClassification.Supported)
+                    {
+                        body = methodSpec.SupportHint is null
+                            ? "throw new System.NotSupportedException();"
+                            : $"throw new System.NotSupportedException(\"{methodSpec.SupportHint}\");";
+                    }
+                    else
+                    {
+                        // Get specific hub connection extension method call
+                        var specificCall = GetSpecificCall(methodSpec);
+
+                        // Handle ValueTask
+                        var prefix = "";
+                        var suffix = "";
+                        if (methodSpec.IsReturnTypeValueTask)
+                        {
+                            if (methodSpec.InnerReturnTypeName is not null)
+                            {
+                                prefix = $"new System.Threading.Tasks.ValueTask<{methodSpec.InnerReturnTypeName}>(";
+                            }
+                            else
+                            {
+                                prefix = "new System.Threading.Tasks.ValueTask(";
+                            }
+                            suffix = $")";
+                        }
+
+                        // Bake it all together
+                        body = $"return {prefix}this.connection.{specificCall}(\"{methodSpec.Name}\"{callArgs}){suffix};";
+                    }
+
+                    var method = $@"
+        {signature}
+        {{
+            {body}
+        }}
+";
+                    methods.Append(method);
+                }
+
+                var proxy = $@"// <auto-generated>
+// Generated by Microsoft.AspNetCore.Client.SourceGenerator
+// </auto-generated>
+
+#nullable enable
+
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace {_spec.GetterNamespace}
+{{
+    {_spec.GetterClassAccessibility} static partial class {_spec.GetterClassName}
+    {{
+        private sealed class {classSpec.ClassTypeName} : {classSpec.FullyQualifiedInterfaceTypeName}
+        {{
+            private readonly HubConnection connection;
+            internal {classSpec.ClassTypeName}(HubConnection connection)
+            {{
+                this.connection = connection;
+            }}
+    {methods.ToString()}
+        }}
+    }}
+}}";
+
+                _context.AddSource($"HubServerProxy.{classSpec.ClassTypeName}.g.cs", SourceText.From(proxy.ToString(), Encoding.UTF8));
+            }
+
+            private string GetSpecificCall(MethodSpec methodSpec)
+            {
+                if (methodSpec.Stream.HasFlag(StreamSpec.ServerToClient) &&
+                    !methodSpec.Stream.HasFlag(StreamSpec.AsyncEnumerable))
+                {
+                    return $"StreamAsChannelAsync<{methodSpec.InnerReturnTypeName}>";
+                }
+
+                if (methodSpec.Stream.HasFlag(StreamSpec.ServerToClient) &&
+                    methodSpec.Stream.HasFlag(StreamSpec.AsyncEnumerable))
+                {
+                    return $"StreamAsync<{methodSpec.InnerReturnTypeName}>";
+                }
+
+                if (methodSpec.InnerReturnTypeName is not null)
+                {
+                    return $"InvokeAsync<{methodSpec.InnerReturnTypeName}>";
+                }
+
+                if (methodSpec.Stream.HasFlag(StreamSpec.ClientToServer))
+                {
+                    return "SendAsync";
+                }
+
+                return "InvokeAsync";
+            }
+        }
+    }
+}

+ 345 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.Parser.cs

@@ -0,0 +1,345 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubServerProxyGenerator
+    {
+        internal class Parser
+        {
+            internal static bool IsSyntaxTargetForAttribute(SyntaxNode node) => node is AttributeSyntax
+            {
+                Name: IdentifierNameSyntax
+                {
+                    Identifier:
+                    {
+                        Text: "HubServerProxy"
+                    }
+                },
+                Parent:
+                {
+                    Parent: MethodDeclarationSyntax
+                    {
+                        Parent: ClassDeclarationSyntax
+                    }
+                }
+            };
+
+            internal static MethodDeclarationSyntax? GetSemanticTargetForAttribute(GeneratorSyntaxContext context)
+            {
+                var attributeSyntax = (AttributeSyntax)context.Node;
+                var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol;
+
+                if (attributeSymbol is null ||
+                    !attributeSymbol.ToString().EndsWith("HubServerProxyAttribute()", StringComparison.Ordinal))
+                {
+                    return null;
+                }
+
+                return (MethodDeclarationSyntax)attributeSyntax.Parent.Parent;
+            }
+
+            private static bool IsExtensionMethodSignatureValid(IMethodSymbol symbol, SourceProductionContext context)
+            {
+                // Check that the method is partial
+                if (!symbol.IsPartialDefinition)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodIsNotPartial,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method is an extension
+                if (!symbol.IsExtensionMethod)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodIsNotExtension,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method has one type parameter
+                if (symbol.Arity != 1)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodTypeArgCountIsBad,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the type parameter matches return type
+                if (!SymbolEqualityComparer.Default.Equals(symbol.TypeArguments[0], symbol.ReturnType))
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodTypeArgAndReturnTypeDoesNotMatch,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                // Check that the method has correct parameters
+                if (symbol.Parameters.Length != 1)
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodArgCountIsBad,
+                        symbol.Locations[0]));
+                    return false;
+                }
+                var hubConnectionSymbol = symbol.Parameters[0].Type as INamedTypeSymbol;
+                if (hubConnectionSymbol.ToString() != "Microsoft.AspNetCore.SignalR.Client.HubConnection")
+                {
+                    context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodArgIsNotHubConnection,
+                        symbol.Locations[0]));
+                    return false;
+                }
+
+                return true;
+            }
+
+            private static bool IsExtensionClassSignatureValid(ClassDeclarationSyntax syntax, SourceProductionContext context)
+            {
+                // Check partialness
+                var hasPartialModifier = false;
+                foreach (var modifier in syntax.Modifiers)
+                {
+                    if (modifier.Kind() == SyntaxKind.PartialKeyword)
+                    {
+                        hasPartialModifier = true;
+                    }
+                }
+                if (!hasPartialModifier)
+                {
+                    return false;
+                }
+
+                return true;
+            }
+
+            internal static bool IsSyntaxTargetForGeneration(SyntaxNode node) => node is MemberAccessExpressionSyntax
+            {
+                Name: GenericNameSyntax
+                {
+                    Arity: 1
+                }
+            };
+
+            internal static MemberAccessExpressionSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
+            {
+                var memberAccessExpressionSyntax = (MemberAccessExpressionSyntax)context.Node;
+
+                if (ModelExtensions.GetSymbolInfo(context.SemanticModel, memberAccessExpressionSyntax).Symbol is not IMethodSymbol
+                    methodSymbol)
+                {
+                    return null;
+                }
+
+                if (!methodSymbol.IsExtensionMethod)
+                {
+                    return null;
+                }
+
+                foreach (var attributeData in methodSymbol.GetAttributes())
+                {
+                    if (!attributeData.AttributeClass.ToString()
+                        .EndsWith("HubServerProxyAttribute", StringComparison.Ordinal))
+                    {
+                        continue;
+                    }
+
+                    return memberAccessExpressionSyntax;
+                }
+
+                return null;
+            }
+
+            private readonly SourceProductionContext _context;
+            private readonly Compilation _compilation;
+
+            internal Parser(SourceProductionContext context, Compilation compilation)
+            {
+                _context = context;
+                _compilation = compilation;
+            }
+
+            internal SourceGenerationSpec Parse(ImmutableArray<MethodDeclarationSyntax> methodDeclarationSyntaxes, ImmutableArray<MemberAccessExpressionSyntax> syntaxList)
+            {
+                // Source generation spec will be populated by type specs for each hub type.
+                // Type specs themselves are populated by method specs which are populated by argument specs.
+                // Source generation spec is then used by emitter to actually generate source.
+                var sourceGenerationSpec = new SourceGenerationSpec();
+
+                // There must be exactly one attributed method
+                if (methodDeclarationSyntaxes.Length != 1)
+                {
+                    // Report diagnostic for each attributed method when there are many
+                    foreach (var extraneous in methodDeclarationSyntaxes)
+                    {
+                        _context.ReportDiagnostic(
+                            Diagnostic.Create(DiagnosticDescriptors.TooManyHubServerProxyAttributedMethods,
+                            extraneous.GetLocation()));
+                    }
+
+                    // nothing to do
+                    return sourceGenerationSpec;
+                }
+
+                var methodDeclarationSyntax = methodDeclarationSyntaxes[0];
+
+                var getProxySemanticModel = _compilation.GetSemanticModel(methodDeclarationSyntax.SyntaxTree);
+                var getProxyMethodSymbol = (IMethodSymbol)getProxySemanticModel.GetDeclaredSymbol(methodDeclarationSyntax);
+                var getProxyClassSymbol = (INamedTypeSymbol)getProxyMethodSymbol.ContainingSymbol;
+
+                // Populate spec with metadata on user-specific get proxy method and class
+                if (!IsExtensionMethodSignatureValid(getProxyMethodSymbol, _context))
+                {
+                    return sourceGenerationSpec;
+                }
+                if (!IsExtensionClassSignatureValid((ClassDeclarationSyntax)methodDeclarationSyntax.Parent, _context))
+                {
+                    return sourceGenerationSpec;
+                }
+
+                sourceGenerationSpec.GetterMethodAccessibility =
+                    GeneratorHelpers.GetAccessibilityString(getProxyMethodSymbol.DeclaredAccessibility);
+                sourceGenerationSpec.GetterClassAccessibility =
+                    GeneratorHelpers.GetAccessibilityString(getProxyClassSymbol.DeclaredAccessibility);
+                if (sourceGenerationSpec.GetterMethodAccessibility is null)
+                {
+                    _context.ReportDiagnostic(Diagnostic.Create(
+                        DiagnosticDescriptors.HubServerProxyAttributedMethodBadAccessibility,
+                        methodDeclarationSyntax.GetLocation()));
+                    return sourceGenerationSpec;
+                }
+                sourceGenerationSpec.GetterMethodName = getProxyMethodSymbol.Name;
+                sourceGenerationSpec.GetterClassName = getProxyClassSymbol.Name;
+                sourceGenerationSpec.GetterNamespace = getProxyClassSymbol.ContainingNamespace.ToString();
+                sourceGenerationSpec.GetterTypeParameterName = getProxyMethodSymbol.TypeParameters[0].Name;
+                sourceGenerationSpec.GetterHubConnectionParameterName = getProxyMethodSymbol.Parameters[0].Name;
+
+                var hubSymbols = new Dictionary<string, (ITypeSymbol, MemberAccessExpressionSyntax)>();
+
+                // Go thru candidates and filter further
+                foreach (var memberAccess in syntaxList)
+                {
+                    var proxyType = ((GenericNameSyntax)memberAccess.Name).TypeArgumentList.Arguments[0];
+
+                    // Filter based on argument symbol
+                    var argumentModel = _compilation.GetSemanticModel(proxyType.SyntaxTree);
+                    if (ModelExtensions.GetSymbolInfo(argumentModel, proxyType).Symbol is not ITypeSymbol { IsAbstract: true } symbol)
+                    {
+                        // T in GetProxy<T> must be an interface
+                        _context.ReportDiagnostic(Diagnostic.Create(
+                            DiagnosticDescriptors.HubServerProxyNonInterfaceGenericTypeArgument,
+                            memberAccess.GetLocation(),
+                            proxyType.ToString()));
+                        continue;
+                    }
+
+                    // Receiver is a HubConnection and argument is abstract so save argument symbol for generation
+                    hubSymbols[symbol.Name] = (symbol, memberAccess);
+                }
+
+                // Generate spec for each proxy
+                foreach (var (hubSymbol, memberAccess) in hubSymbols.Values)
+                {
+                    var classSpec = new ClassSpec
+                    {
+                        FullyQualifiedInterfaceTypeName = hubSymbol.ToString(),
+                        ClassTypeName = $"Generated{hubSymbol.Name}",
+                        CallSite = memberAccess.GetLocation()
+                    };
+
+                    var members = hubSymbol.GetMembers()
+                        .Where(member => member.Kind == SymbolKind.Method)
+                        .Select(member => (IMethodSymbol)member)
+                        .Concat(hubSymbol.AllInterfaces.SelectMany(x => x
+                            .GetMembers()
+                            .Where(member => member.Kind == SymbolKind.Method)
+                            .Select(member => (IMethodSymbol)member)));
+
+                    // Generate spec for each method
+                    foreach (var member in members)
+                    {
+                        var methodSpec = new MethodSpec
+                        {
+                            Name = member.Name,
+                            FullyQualifiedReturnTypeName = member.ReturnType.ToString()
+                        };
+
+                        if (member.ReturnType is INamedTypeSymbol { Arity: 1 } rtype)
+                        {
+                            methodSpec.InnerReturnTypeName = rtype.TypeArguments[0].ToString();
+                        }
+
+                        if (member.ReturnType is INamedTypeSymbol { Arity: 1, Name: "Task" } a
+                            && a.TypeArguments[0] is INamedTypeSymbol { Arity: 1, Name: "ChannelReader" } b)
+                        {
+                            methodSpec.Stream = StreamSpec.ServerToClient & ~StreamSpec.AsyncEnumerable;
+                            methodSpec.InnerReturnTypeName = b.TypeArguments[0].ToString();
+                        }
+                        else if (member.ReturnType is INamedTypeSymbol { Arity: 1, Name: "IAsyncEnumerable" } c)
+                        {
+                            methodSpec.Stream = StreamSpec.ServerToClient | StreamSpec.AsyncEnumerable;
+                            methodSpec.InnerReturnTypeName = c.TypeArguments[0].ToString();
+                        }
+                        else
+                        {
+                            methodSpec.Stream = StreamSpec.None;
+                        }
+
+                        // Generate spec for each argument
+                        foreach (var parameter in member.Parameters)
+                        {
+                            var argumentSpec = new ArgumentSpec
+                            {
+                                Name = parameter.Name,
+                                FullyQualifiedTypeName = parameter.Type.ToString()
+                            };
+
+                            methodSpec.Arguments.Add(argumentSpec);
+
+                            switch (parameter.Type)
+                            {
+                                case INamedTypeSymbol { Arity: 1, Name: "ChannelReader" }:
+                                    methodSpec.Stream |= StreamSpec.ClientToServer;
+                                    break;
+                                case INamedTypeSymbol { Arity: 1, Name: "IAsyncEnumerable" }:
+                                    methodSpec.Stream |= StreamSpec.ClientToServer;
+                                    break;
+                            }
+                        }
+
+                        // Validate return type
+                        if (!methodSpec.Stream.HasFlag(StreamSpec.ServerToClient) &&
+                            member.ReturnType is not INamedTypeSymbol { Name: "Task" or "ValueTask" })
+                        {
+                            _context.ReportDiagnostic(Diagnostic.Create(
+                                    DiagnosticDescriptors.HubServerProxyUnsupportedReturnType,
+                                    classSpec.CallSite,
+                                    methodSpec.Name, member.ReturnType.Name));
+                            methodSpec.Support = SupportClassification.UnsupportedReturnType;
+                            methodSpec.SupportHint = "Return type must be Task, ValueTask, Task<T> or ValueTask<T>";
+                        }
+
+                        classSpec.Methods.Add(methodSpec);
+                    }
+
+                    sourceGenerationSpec.Classes.Add(classSpec);
+                }
+
+                return sourceGenerationSpec;
+            }
+        }
+    }
+}

+ 68 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.SourceGenerationSpec.cs

@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    internal partial class HubServerProxyGenerator
+    {
+        public class SourceGenerationSpec
+        {
+            public string? GetterNamespace;
+            public string? GetterClassName;
+            public string? GetterMethodName;
+            public string? GetterTypeParameterName;
+            public string? GetterHubConnectionParameterName;
+            public string? GetterMethodAccessibility;
+            public string? GetterClassAccessibility;
+            public List<ClassSpec> Classes = new();
+        }
+
+        public class ClassSpec
+        {
+            public string FullyQualifiedInterfaceTypeName;
+            public string ClassTypeName;
+            public List<MethodSpec> Methods = new();
+            public Location CallSite;
+        }
+
+        public class MethodSpec
+        {
+            public string Name;
+            public string FullyQualifiedReturnTypeName;
+            public List<ArgumentSpec> Arguments = new();
+            public SupportClassification Support;
+            public string? SupportHint;
+            public StreamSpec Stream;
+            public string? InnerReturnTypeName;
+            public bool IsReturnTypeValueTask => FullyQualifiedReturnTypeName
+                .StartsWith("System.Threading.Tasks.ValueTask", StringComparison.Ordinal);
+        }
+
+        [Flags]
+        public enum StreamSpec
+        {
+            None = 0,
+            ClientToServer = 1,
+            ServerToClient = 2,
+            AsyncEnumerable = 4,
+            Bidirectional = ClientToServer | ServerToClient
+        }
+
+        public enum SupportClassification
+        {
+            Supported,
+            UnsupportedReturnType
+        }
+
+        public class ArgumentSpec
+        {
+            public string Name;
+            public string FullyQualifiedTypeName;
+        }
+    }
+}

+ 45 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/HubServerProxyGenerator.cs

@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
+{
+    [Generator]
+    internal partial class HubServerProxyGenerator : IIncrementalGenerator
+    {
+        public void Initialize(IncrementalGeneratorInitializationContext context)
+        {
+            var methodDeclaration = context.SyntaxProvider
+                .CreateSyntaxProvider(static (s, _) => Parser.IsSyntaxTargetForAttribute(s),
+                    static (ctx, _) => Parser.GetSemanticTargetForAttribute(ctx))
+                .Where(static m => m is not null)
+                .Collect();
+
+            var memberAccessExpressions = context.SyntaxProvider
+                .CreateSyntaxProvider(static (s, _) => Parser.IsSyntaxTargetForGeneration(s),
+                    static (ctx, _) => Parser.GetSemanticTargetForGeneration(ctx))
+                .Where(static m => m is not null);
+
+            var compilationAndMethodDeclaration = context.CompilationProvider.Combine(methodDeclaration);
+
+            var payload = compilationAndMethodDeclaration
+                .Combine(memberAccessExpressions.Collect());
+
+            context.RegisterSourceOutput(payload, static (spc, source) =>
+                Execute(source.Left.Left, source.Left.Right, source.Right, spc));
+        }
+
+        private static void Execute(Compilation compilation, ImmutableArray<MethodDeclarationSyntax> methodDeclarationSyntaxes, ImmutableArray<MemberAccessExpressionSyntax> memberAccessExpressionSyntaxes, SourceProductionContext context)
+        {
+            var parser = new Parser(context, compilation);
+            var spec = parser.Parse(methodDeclarationSyntaxes, memberAccessExpressionSyntaxes);
+            var emitter = new Emitter(context, spec);
+            emitter.Emit();
+        }
+    }
+}

+ 19 - 0
src/SignalR/clients/csharp/Client.SourceGenerator/src/Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>9</LangVersion>
+    <IncludeBuildOutput>false</IncludeBuildOutput>
+    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+  </ItemGroup>
+
+</Project>

+ 263 - 0
src/SignalR/clients/csharp/Client/test/UnitTests/HubClientProxyGeneratorTests.cs

@@ -0,0 +1,263 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.SignalR.Client.Tests
+{
+    [AttributeUsage(AttributeTargets.Method)]
+    internal class HubClientProxyAttribute : Attribute
+    {
+
+    }
+
+    internal static partial class RegisterCallbackProviderExtensions
+    {
+        [HubClientProxy]
+        public static partial IDisposable SetHubClient<T>(this HubConnection conn, T provider);
+    }
+
+    public class HubClientProxyGeneratorTests
+    {
+        public interface IMyClient
+        {
+            void NoArg();
+            void SingleArg(int a);
+            void ManyArgs(int a, float b, int? c);
+            Task ReturnTask();
+        }
+
+        private class MyClient : IMyClient
+        {
+            public int CallsOfNoArg;
+            public void NoArg()
+            {
+                CallsOfNoArg += 1;
+            }
+
+            public List<int> CallsOfSingleArg = new();
+            public void SingleArg(int a)
+            {
+                CallsOfSingleArg.Add(a);
+            }
+
+            public List<(int, float, int?)> CallsOfManyArgs = new();
+            public void ManyArgs(int a, float b, int? c)
+            {
+                CallsOfManyArgs.Add((a, b, c));
+            }
+
+            public int CallsOfReturnTask;
+            public Task ReturnTask()
+            {
+                CallsOfReturnTask += 1;
+                return Task.CompletedTask;
+            }
+        }
+
+        private class Disposable : IDisposable
+        {
+            public bool IsDisposed;
+            public void Dispose() => IsDisposed = true;
+        }
+
+        [Fact]
+        public void RegistersCallbackProvider()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            var noArgReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "NoArg",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(noArgReg);
+            var singleArgReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "SingleArg",
+                    It.Is<Type[]>(t => t.Length == 1 && t[0] == typeof(int)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(singleArgReg);
+            var manyArgsReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "ManyArgs",
+                    It.Is<Type[]>(t => t.Length == 3 && t[0] == typeof(int) && t[1] == typeof(float) && t[2] == typeof(int?)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(manyArgsReg);
+            var returnTaskReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "ReturnTask",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(returnTaskReg);
+            var conn = mockConn.Object;
+            var myClient = new MyClient();
+
+            // Act
+            var registration = conn.SetHubClient<IMyClient>(myClient);
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.False(noArgReg.IsDisposed);
+            Assert.False(singleArgReg.IsDisposed);
+            Assert.False(manyArgsReg.IsDisposed);
+            Assert.False(returnTaskReg.IsDisposed);
+        }
+
+        [Fact]
+        public void UnregistersCallbackProvider()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            var noArgReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "NoArg",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(noArgReg);
+            var singleArgReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "SingleArg",
+                    It.Is<Type[]>(t => t.Length == 1 && t[0] == typeof(int)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(singleArgReg);
+            var manyArgsReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "ManyArgs",
+                    It.Is<Type[]>(t => t.Length == 3 && t[0] == typeof(int) && t[1] == typeof(float) && t[2] == typeof(int?)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(manyArgsReg);
+            var returnTaskReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "ReturnTask",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Returns(returnTaskReg);
+            var conn = mockConn.Object;
+            var myClient = new MyClient();
+            var registration = conn.SetHubClient<IMyClient>(myClient);
+
+            // Act
+            registration.Dispose();
+
+            // Assert
+            Assert.True(noArgReg.IsDisposed);
+            Assert.True(singleArgReg.IsDisposed);
+            Assert.True(manyArgsReg.IsDisposed);
+            Assert.True(returnTaskReg.IsDisposed);
+        }
+
+        [Fact]
+        public async Task CallbacksGetTriggered()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            var noArgReg = new Disposable();
+            Func<object[], object, Task> noArgFunc = null;
+            object noArgState = null;
+            mockConn
+                .Setup(x => x.On(
+                    "NoArg",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Callback(
+                    (string methodName, Type[] parameterTypes, Func<object[], object, Task> handler, object state) =>
+                    {
+                        noArgFunc = handler;
+                        noArgState = state;
+                    })
+                .Returns(noArgReg);
+            Func<object[], object, Task> singleArgFunc = null;
+            object singleArgState = null;
+            var singleArgReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "SingleArg",
+                    It.Is<Type[]>(t => t.Length == 1 && t[0] == typeof(int)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Callback(
+                    (string methodName, Type[] parameterTypes, Func<object[], object, Task> handler, object state) =>
+                    {
+                        singleArgFunc = handler;
+                        singleArgState = state;
+                    })
+                .Returns(singleArgReg);
+            Func<object[], object, Task> manyArgsFunc = null;
+            object manyArgsState = null;
+            var manyArgsReg = new Disposable();
+            mockConn
+                .Setup(x => x.On(
+                    "ManyArgs",
+                    It.Is<Type[]>(t => t.Length == 3 && t[0] == typeof(int) && t[1] == typeof(float) && t[2] == typeof(int?)),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Callback(
+                    (string methodName, Type[] parameterTypes, Func<object[], object, Task> handler, object state) =>
+                    {
+                        manyArgsFunc = handler;
+                        manyArgsState = state;
+                    })
+                .Returns(manyArgsReg);
+            var returnTaskReg = new Disposable();
+            Func<object[], object, Task> returnTaskFunc = null;
+            object returnTaskState = null;
+            mockConn
+                .Setup(x => x.On(
+                    "ReturnTask",
+                    Array.Empty<Type>(),
+                    It.IsAny<Func<object[], object, Task>>(),
+                    It.IsAny<object>()))
+                .Callback(
+                    (string methodName, Type[] parameterTypes, Func<object[], object, Task> handler, object state) =>
+                    {
+                        returnTaskFunc = handler;
+                        returnTaskState = state;
+                    })
+                .Returns(returnTaskReg);
+            var conn = mockConn.Object;
+            var myClient = new MyClient();
+            var registration = conn.SetHubClient<IMyClient>(myClient);
+
+            // Act + Assert
+            Assert.NotNull(noArgFunc);
+            await noArgFunc(Array.Empty<object>(), noArgState);
+            Assert.Equal(1, myClient.CallsOfNoArg);
+
+            Assert.NotNull(singleArgFunc);
+            await singleArgFunc(new object[]{10}, singleArgState);
+            Assert.Single(myClient.CallsOfSingleArg);
+            Assert.Equal(10, myClient.CallsOfSingleArg[0]);
+
+            Assert.NotNull(manyArgsFunc);
+            await singleArgFunc(new object[]{10, 5.5f, null}, manyArgsState);
+            Assert.Single(myClient.CallsOfManyArgs);
+            Assert.Equal((10, 5.5f, null), myClient.CallsOfManyArgs[0]);
+
+            Assert.NotNull(returnTaskFunc);
+            await returnTaskFunc(Array.Empty<object>(), returnTaskState);
+            Assert.Equal(1, myClient.CallsOfReturnTask);
+        }
+    }
+}

+ 337 - 0
src/SignalR/clients/csharp/Client/test/UnitTests/HubServerProxyGeneratorTests.cs

@@ -0,0 +1,337 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.SignalR.Client.Tests
+{
+    [AttributeUsage(AttributeTargets.Method)]
+    internal class HubServerProxyAttribute : Attribute
+    {
+
+    }
+
+    internal static partial class HubServerProxyExtensions
+    {
+        [HubServerProxy]
+        public static partial T GetHubServer<T>(this HubConnection conn);
+    }
+
+    public class HubServerProxyGeneratorTests
+    {
+        public interface IMyHub
+        {
+            Task GetNothing();
+            Task<int> GetScalar();
+            Task<List<int>> GetCollection();
+            Task<int> SetScalar(int a);
+            Task<List<int>> SetCollection(List<int> a);
+            Task<ChannelReader<int>> StreamToClientViaChannel();
+            Task<ChannelReader<int>> StreamToClientViaChannelWithToken(CancellationToken cancellationToken);
+            IAsyncEnumerable<int> StreamToClientViaEnumerableWithToken(CancellationToken cancellationToken);
+            Task StreamFromClientViaChannel(ChannelReader<int> reader);
+            Task StreamFromClientViaEnumerable(IAsyncEnumerable<int> reader);
+            Task<int> StreamFromClientButAlsoReturnValue(ChannelReader<int> reader);
+            Task<ChannelReader<int>> StreamBidirectionalViaChannel(ChannelReader<float> reader);
+            Task<ChannelReader<int>> StreamBidirectionalViaChannelWithToken(ChannelReader<float> reader, CancellationToken cancellationToken);
+            IAsyncEnumerable<int> StreamBidirectionalViaEnumerable(IAsyncEnumerable<float> reader);
+            IAsyncEnumerable<int> StreamBidirectionalViaEnumerableWithToken(IAsyncEnumerable<float> reader, CancellationToken cancellationToken);
+            ValueTask ReturnValueTask();
+            ValueTask<int> ReturnGenericValueTask();
+            Task<int?> HandleNullables(float? nullable);
+        }
+
+        [Fact]
+        public async Task GetNothing()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.GetNothing),
+                    typeof(object),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult(default(object)));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            await myHub.GetNothing();
+
+            // Assert
+            mockConn.VerifyAll();
+        }
+
+        [Fact]
+        public async Task GetScalar()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.GetScalar),
+                    typeof(int),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult((object) 10));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            var result = await myHub.GetScalar();
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.Equal(10, result);
+        }
+
+        [Fact]
+        public async Task GetCollection()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.GetCollection),
+                    typeof(List<int>),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult((object) new List<int>{ 10 }));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            var result = await myHub.GetCollection();
+
+            // hello
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.NotNull(result);
+            Assert.Collection(result, item => Assert.Equal(10, item));
+        }
+
+        [Fact]
+        public async Task SetScalar()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.SetScalar),
+                    typeof(int),
+                    It.Is<object[]>(y => ((object[])y).Any(z => (int)z == 20)),
+                    default))
+                .Returns(Task.FromResult((object) 10));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            var result = await myHub.SetScalar(20);
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.Equal(10, result);
+        }
+
+        [Fact]
+        public async Task SetCollection()
+        {
+            // Arrange
+            var arg = new List<int>() {20};
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.SetCollection),
+                    typeof(List<int>),
+                    It.Is<object[]>(y => ((object[])y).Any(z => (List<int>)z == arg)),
+                    default))
+                .Returns(Task.FromResult((object) new List<int>{ 10 }));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            var result = await myHub.SetCollection(arg);
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.NotNull(result);
+            Assert.Collection(result, item => Assert.Equal(10, item));
+        }
+
+        [Fact]
+        public async Task StreamToClient()
+        {
+            // Arrange
+            var channel = Channel.CreateUnbounded<object>();
+            var channelForEnumerable = Channel.CreateUnbounded<int>();
+            var asyncEnumerable = channelForEnumerable.Reader.ReadAllAsync();
+            var cts = new CancellationTokenSource();
+            var token = cts.Token;
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.StreamAsChannelCoreAsync(
+                    nameof(IMyHub.StreamToClientViaChannel),
+                    typeof(int),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult(channel.Reader));
+            mockConn
+                .Setup(x => x.StreamAsChannelCoreAsync(
+                    nameof(IMyHub.StreamToClientViaChannelWithToken),
+                    typeof(int),
+                    Array.Empty<object>(),
+                    token))
+                .Returns(Task.FromResult(channel.Reader));
+            mockConn
+                .Setup(x => x.StreamAsyncCore<int>(
+                    nameof(IMyHub.StreamToClientViaEnumerableWithToken),
+                    Array.Empty<object>(),
+                    token))
+                .Returns(asyncEnumerable);
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            _ = await myHub.StreamToClientViaChannel();
+            _ = await myHub.StreamToClientViaChannelWithToken(token);
+            _ = myHub.StreamToClientViaEnumerableWithToken(token);
+
+            // Assert
+            mockConn.VerifyAll();
+        }
+
+        [Fact]
+        public async Task StreamFromClient()
+        {
+            // Arrange
+            var channel = Channel.CreateUnbounded<int>();
+            var channelReader = channel.Reader;
+            var channelForEnumerable = Channel.CreateUnbounded<int>();
+            var asyncEnumerable = channelForEnumerable.Reader.ReadAllAsync();
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.SendCoreAsync(
+                    nameof(IMyHub.StreamFromClientViaChannel),
+                    It.Is<object[]>(y => ((object[])y).Any(z => (ChannelReader<int>)z == channelReader)),
+                    default))
+                .Returns(Task.CompletedTask);
+            mockConn
+                .Setup(x => x.SendCoreAsync(
+                    nameof(IMyHub.StreamFromClientViaEnumerable),
+                    It.Is<object[]>(y => ((object[])y).Any(z => (IAsyncEnumerable<int>)z == asyncEnumerable)),
+                    default))
+                .Returns(Task.CompletedTask);
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.StreamFromClientButAlsoReturnValue),
+                    typeof(int),
+                    It.Is<object[]>(y => ((object[])y).Any(z => (ChannelReader<int>)z == channelReader)),
+                    default))
+                .Returns(Task.FromResult((object) 6));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            await myHub.StreamFromClientViaChannel(channelReader);
+            await myHub.StreamFromClientViaEnumerable(asyncEnumerable);
+            var result = await myHub.StreamFromClientButAlsoReturnValue(channelReader);
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.Equal(6, result);
+        }
+
+        [Fact]
+        public async Task BidirectionalStream()
+        {
+            // Arrange
+            var argChannel = Channel.CreateUnbounded<float>();
+            var retChannel = Channel.CreateUnbounded<object>();
+            var retChannelReader = retChannel.Reader;
+            var argChannelForEnumerable = Channel.CreateUnbounded<float>();
+            var argEnumerable = argChannelForEnumerable.Reader.ReadAllAsync();
+            var retChannelForEnumerable = Channel.CreateUnbounded<int>();
+            var retEnumerable = retChannelForEnumerable.Reader.ReadAllAsync();
+            var cts = new CancellationTokenSource();
+            var token = cts.Token;
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.StreamAsChannelCoreAsync(
+                    nameof(IMyHub.StreamBidirectionalViaChannel),
+                    typeof(int),
+                    It.Is<object[]>(y => ((object[]) y).Any(z => z is ChannelReader<float>)),
+                    default))
+                .Returns(Task.FromResult(retChannelReader));
+            mockConn
+                .Setup(x => x.StreamAsChannelCoreAsync(
+                    nameof(IMyHub.StreamBidirectionalViaChannelWithToken),
+                    typeof(int),
+                    It.Is<object[]>(y => ((object[]) y).Any(z => z is ChannelReader<float>)),
+                    token))
+                .Returns(Task.FromResult(retChannelReader));
+            mockConn
+                .Setup(x => x.StreamAsyncCore<int>(
+                    nameof(IMyHub.StreamBidirectionalViaEnumerable),
+                    It.Is<object[]>(y => ((object[]) y).Any(z => z is IAsyncEnumerable<float>)),
+                    default))
+                .Returns(retEnumerable);
+            mockConn
+                .Setup(x => x.StreamAsyncCore<int>(
+                    nameof(IMyHub.StreamBidirectionalViaEnumerableWithToken),
+                    It.Is<object[]>(y => ((object[]) y).Any(z => z is IAsyncEnumerable<float>)),
+                    token))
+                .Returns(retEnumerable);
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            _ = await myHub.StreamBidirectionalViaChannel(argChannel.Reader);
+            _ = await myHub.StreamBidirectionalViaChannelWithToken(argChannel.Reader, token);
+            _ = myHub.StreamBidirectionalViaEnumerable(argEnumerable);
+            _ = myHub.StreamBidirectionalViaEnumerableWithToken(argEnumerable, token);
+
+            // Assert
+            mockConn.VerifyAll();
+        }
+
+        [Fact]
+        public async Task ReturnValueTask()
+        {
+            // Arrange
+            var mockConn = MockHubConnection.Get();
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.ReturnValueTask),
+                    typeof(object),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult(default(object)));
+            mockConn
+                .Setup(x => x.InvokeCoreAsync(
+                    nameof(IMyHub.ReturnGenericValueTask),
+                    typeof(int),
+                    Array.Empty<object>(),
+                    default))
+                .Returns(Task.FromResult((object) 10));
+            var conn = mockConn.Object;
+            var myHub = conn.GetHubServer<IMyHub>();
+
+            // Act
+            await myHub.ReturnValueTask();
+            var result = await myHub.ReturnGenericValueTask();
+
+            // Assert
+            mockConn.VerifyAll();
+            Assert.Equal(10, result);
+        }
+    }
+}

+ 2 - 0
src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj

@@ -20,6 +20,8 @@
     <Reference Include="Microsoft.Extensions.Logging" />
     <Reference Include="Microsoft.AspNetCore.TestHost" />
     <Reference Include="Microsoft.AspNetCore.SignalR" />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Client.Core" />
+    <Reference Include="Microsoft.AspNetCore.SignalR.Client.SourceGenerator" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
   </ItemGroup>
 
 </Project>

+ 26 - 0
src/SignalR/clients/csharp/Client/test/UnitTests/MockHubConnection.cs

@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.SignalR.Protocol;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace Microsoft.AspNetCore.SignalR.Client.Tests
+{
+    public static class MockHubConnection
+    {
+        public static Mock<HubConnection> Get()
+        {
+            IConnectionFactory connectionFactory = new Mock<IConnectionFactory>().Object;
+            IHubProtocol protocol = new Mock<IHubProtocol>().Object;
+            EndPoint endPoint = new Mock<EndPoint>().Object;
+            IServiceProvider serviceProvider = new Mock<IServiceProvider>().Object;
+            ILoggerFactory loggerFactory = null;
+            return new Mock<HubConnection>(MockBehavior.Strict,
+                connectionFactory, protocol, endPoint, serviceProvider, loggerFactory);
+        }
+    }
+}