| File: HubClientProxyGenerator.Emitter.cs | Web Access |
| Project: src\aspnetcore\src\SignalR\clients\csharp\Client.SourceGenerator\src\Microsoft.AspNetCore.SignalR.Client.SourceGenerator.csproj (Microsoft.AspNetCore.SignalR.Client.SourceGenerator) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator; internal partial class HubClientProxyGenerator { public sealed 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}) {_spec.SetterProviderParameterName})); }}"); } // 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 = GeneratorHelpers.SourceFilePrefix() + $@" 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(GeneratorHelpers.SourceFilePrefix() + $@" 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)); } } }