From a51e2b0608d02d25ef3dc6aa98ec75dc82aaa094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 09:51:58 +0200 Subject: [PATCH 01/25] First approach with incorectly reused generator --- src/IntegrationTestApp/Program.cs | 12 -- .../ConfigurationAnalyzer.cs | 152 ++---------------- .../DiagnosticIds.cs | 9 ++ .../FunctionCompositionGenerator.Emitter.cs | 4 + .../FunctionCompositionGenerator.Parser.cs | 4 +- ...ctionCompositionGenerator.TrackingNames.cs | 3 +- .../FunctionCompositionGenerator.cs | 12 +- ...nctionEndpointGenerator.AzureServiceBus.cs | 3 + .../FunctionEndpointGenerator.Emitter.cs | 40 ++++- .../FunctionEndpointGenerator.Parser.cs | 138 ++++++++++++++++ ...FunctionEndpointGenerator.TrackingNames.cs | 6 +- .../FunctionEndpointGenerator.cs | 38 ++++- .../KnownTypeNames.cs | 5 +- ...nctionsHostApplicationBuilderExtensions.cs | 22 ++- .../FunctionEndpointConfigurationBuilder.cs | 19 ++- ...nctionsHostApplicationBuilderExtensions.cs | 40 ----- ...veAzureServiceBusComponentApi.approved.txt | 1 + ....ApproveFunctionsComponentApi.approved.txt | 23 +-- ...s.GeneratesProjectComposition.approved.txt | 13 ++ ...apeAllowsAdditionalParameters.approved.txt | 9 ++ ...EndpointWithoutMessageActions.approved.txt | 9 ++ ...sts.GeneratesFunctionEndpoint.approved.txt | 9 ++ ...tionEndpointInGlobalNamespace.approved.txt | 9 ++ .../ConfigurationAnalyzerTests.cs | 33 ++-- .../FunctionEndpointGeneratorTests.cs | 47 ++++++ .../LenientNoMessageActionsGenerator.cs | 3 +- .../NoMessageActionsGenerator.cs | 3 +- src/Tests.Analyzers/TestSources.cs | 5 + 28 files changed, 415 insertions(+), 256 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.Common/FunctionsHostApplicationBuilderExtensions.cs diff --git a/src/IntegrationTestApp/Program.cs b/src/IntegrationTestApp/Program.cs index e2fef745..ffce51d9 100644 --- a/src/IntegrationTestApp/Program.cs +++ b/src/IntegrationTestApp/Program.cs @@ -17,18 +17,6 @@ builder.AddNServiceBusFunctions(); -builder.AddSendOnlyNServiceBusEndpoint("client", (configuration, services) => -{ - services.AddSingleton(new MyComponent("client")); - - var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default) { ConnectionName = "AzureWebJobsServiceBus" }; - - var routing = configuration.UseTransport(transport); - - routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); - configuration.UseSerialization(); -}); - var host = builder.Build(); await host.RunAsync(); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs index 3f4c73bb..e6e89ef6 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs @@ -36,9 +36,7 @@ static void OnCompilationStart(CompilationStartAnalysisContext context) context.Compilation.GetTypeByMetadataName(KnownTypeNames.SendOptions), context.Compilation.GetTypeByMetadataName(KnownTypeNames.ReplyOptions), context.Compilation.GetTypeByMetadataName(KnownTypeNames.AzureServiceBusServerlessTransport), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.ActionOfT), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.ActionOfT1T2), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.FunctionsHostApplicationBuilderExtensions)); + context.Compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute)); var sendOnlyConfigureMethods = new ConcurrentDictionary(SymbolEqualityComparer.Default); var deferredInvocations = new ConcurrentBag(); @@ -50,18 +48,17 @@ static void OnCompilationStart(CompilationStartAnalysisContext context) { if (blockStartContext.OwningSymbol is IMethodSymbol method && HasSupportedConfigureMethodSignature(method, knownSymbols)) { + if (HasSendOnlyEndpointAttribute(method, knownSymbols.SendOnlyEndpointAttribute)) + { + sendOnlyConfigureMethods.TryAdd(method.OriginalDefinition, true); + } + blockStartContext.RegisterSyntaxNodeAction( nodeContext => CollectEndpointConfigurationInvocation(nodeContext, method, deferredInvocations), SyntaxKind.InvocationExpression); } }); - // Track method group arguments to AddSendOnlyNServiceBusEndpoint and analyze - // EndpointConfiguration calls inside lambda callbacks (syntax-tree walking). - context.RegisterSyntaxNodeAction( - nodeContext => AnalyzeSendOnlyInvocation(nodeContext, knownSymbols, sendOnlyConfigureMethods), - SyntaxKind.InvocationExpression); - // SendOptions/ReplyOptions: syntax-node-scoped, purely receiver-type checked. context.RegisterSyntaxNodeAction( nodeContext => AnalyzeSendAndReplyOptions(nodeContext, knownSymbols), @@ -133,62 +130,6 @@ static void AnalyzeEndpointConfiguration( GetEndpointConfigurationReason(rule, endpointContext))); } - static void AnalyzeSendOnlyInvocation( - SyntaxNodeAnalysisContext context, - KnownSymbols knownSymbols, - ConcurrentDictionary sendOnlyConfigureMethods) - { - if (context.Node is not InvocationExpressionSyntax invocationExpression) - { - return; - } - - if (IsAddSendOnlyNServiceBusEndpoint(context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken).Symbol, knownSymbols) - && invocationExpression.ArgumentList.Arguments.Count >= 2) - { - var lastArgument = invocationExpression.ArgumentList.Arguments[^1].Expression; - - if (context.SemanticModel.GetSymbolInfo(lastArgument, context.CancellationToken).Symbol is IMethodSymbol referencedMethod - && HasSupportedConfigureMethodSignature(referencedMethod, knownSymbols)) - { - sendOnlyConfigureMethods.TryAdd(referencedMethod.OriginalDefinition, true); - } - } - - if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression }) - { - return; - } - - if (!IsEndpointConfigurationReceiver(memberAccessExpression, context.SemanticModel, knownSymbols.EndpointConfiguration, context.CancellationToken)) - { - return; - } - - if (!IsInsideSendOnlyCallback(invocationExpression, context.SemanticModel, knownSymbols, context.CancellationToken)) - { - return; - } - - if (memberAccessExpression.Name.Identifier.ValueText == UseTransportMethodName) - { - AnalyzeInvalidTransportConfiguration(invocationExpression, context.SemanticModel, context.ReportDiagnostic, EndpointConfigurationContext.SendOnlyEndpoint, knownSymbols, context.CancellationToken); - return; - } - - if (!InvalidEndpointConfigurationMethods.TryGetValue(memberAccessExpression.Name.Identifier.ValueText, out var rule)) - { - return; - } - - context.ReportDiagnostic(Diagnostic.Create( - DiagnosticIds.InvalidEndpointConfigurationDescriptor, - invocationExpression.GetLocation(), - rule.ApiName, - GetEndpointContextLabel(EndpointConfigurationContext.SendOnlyEndpoint), - GetEndpointConfigurationReason(rule, EndpointConfigurationContext.SendOnlyEndpoint))); - } - static void AnalyzeInvalidTransportConfiguration( InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, @@ -214,24 +155,6 @@ static void AnalyzeInvalidTransportConfiguration( GetEndpointContextLabel(endpointContext))); } - static bool IsInsideSendOnlyCallback( - InvocationExpressionSyntax invocationExpression, - SemanticModel semanticModel, - KnownSymbols knownSymbols, - CancellationToken cancellationToken) - { - for (SyntaxNode? current = invocationExpression; current is not null; current = current.Parent) - { - if (current is AnonymousFunctionExpressionSyntax anonymousFunction - && IsSendOnlyEndpointConfigurationCallback(anonymousFunction, semanticModel, knownSymbols, cancellationToken)) - { - return true; - } - } - - return false; - } - static void AnalyzeSendAndReplyOptions(SyntaxNodeAnalysisContext context, KnownSymbols knownSymbols) { if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression } invocationExpression) @@ -259,20 +182,6 @@ static void AnalyzeSendAndReplyOptions(SyntaxNodeAnalysisContext context, KnownS rule.Reason)); } - static bool IsAddSendOnlyNServiceBusEndpoint(ISymbol? symbol, KnownSymbols knownSymbols) - => symbol is IMethodSymbol { Name: KnownTypeNames.AddSendOnlyNServiceBusEndpoint, Parameters.Length: 2 } method - && ContainsType(method, knownSymbols.FunctionsHostApplicationBuilderExtensions); - - static bool IsEndpointConfigurationReceiver( - MemberAccessExpressionSyntax memberAccessExpression, - SemanticModel semanticModel, - INamedTypeSymbol? endpointConfigurationSymbol, - CancellationToken cancellationToken) - { - var receiverType = semanticModel.GetTypeInfo(memberAccessExpression.Expression, cancellationToken).Type; - return SymbolEqualityComparer.Default.Equals(receiverType, endpointConfigurationSymbol); - } - static bool HasSupportedConfigureMethodSignature(IMethodSymbol method, KnownSymbols knownSymbols) { if (method.Parameters.Length == 0 @@ -292,53 +201,16 @@ static bool HasSupportedConfigureMethodSignature(IMethodSymbol method, KnownSymb return true; } - static bool IsSendOnlyEndpointConfigurationCallback( - AnonymousFunctionExpressionSyntax anonymousFunction, - SemanticModel semanticModel, - KnownSymbols knownSymbols, - CancellationToken cancellationToken) - { - if (anonymousFunction.Parent is not ArgumentSyntax { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax invocationExpression } argumentList }) - { - return false; - } - - if (argumentList.Arguments.Count == 0 || argumentList.Arguments[^1].Expression != anonymousFunction) - { - return false; - } - - if (semanticModel.GetSymbolInfo(invocationExpression, cancellationToken).Symbol is not IMethodSymbol methodSymbol - || !IsAddSendOnlyNServiceBusEndpoint(methodSymbol, knownSymbols)) - { - return false; - } - - return methodSymbol.Parameters[1].Type is INamedTypeSymbol { TypeArguments.Length: 1 or 2 } delegateType - && IsSupportedSendOnlyCallbackDelegate(delegateType, knownSymbols) - && SymbolEqualityComparer.Default.Equals(delegateType.TypeArguments[0], knownSymbols.EndpointConfiguration) - && (delegateType.TypeArguments.Length == 1 - || SymbolEqualityComparer.Default.Equals(delegateType.TypeArguments[1], knownSymbols.IServiceCollection)); - } - - static bool IsSupportedSendOnlyCallbackDelegate(INamedTypeSymbol delegateType, KnownSymbols knownSymbols) - => delegateType.TypeArguments.Length switch - { - 1 => SymbolEqualityComparer.Default.Equals(delegateType.OriginalDefinition, knownSymbols.ActionOfT), - 2 => SymbolEqualityComparer.Default.Equals(delegateType.OriginalDefinition, knownSymbols.ActionOfT1T2), - _ => false - }; - - static bool ContainsType(IMethodSymbol method, INamedTypeSymbol? target) + static bool HasSendOnlyEndpointAttribute(IMethodSymbol method, INamedTypeSymbol? sendOnlyEndpointAttribute) { - if (target is null) + if (sendOnlyEndpointAttribute is null) { return false; } - for (INamedTypeSymbol? type = method.ContainingType; type is not null; type = type.ContainingType) + foreach (var attribute in method.GetAttributes()) { - if (SymbolEqualityComparer.Default.Equals(type, target)) + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, sendOnlyEndpointAttribute)) { return true; } @@ -408,9 +280,7 @@ readonly record struct KnownSymbols( INamedTypeSymbol? SendOptions, INamedTypeSymbol? ReplyOptions, INamedTypeSymbol? AzureServiceBusServerlessTransport, - INamedTypeSymbol? ActionOfT, - INamedTypeSymbol? ActionOfT1T2, - INamedTypeSymbol? FunctionsHostApplicationBuilderExtensions); + INamedTypeSymbol? SendOnlyEndpointAttribute); enum EndpointConfigurationContext { diff --git a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs index f2b416e8..f11da031 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs @@ -17,6 +17,7 @@ static class DiagnosticIds public const string InvalidEndpointConfiguration = "NSBFUNC008"; public const string InvalidSendOptions = "NSBFUNC009"; public const string InvalidEndpointTransportConfiguration = "NSBFUNC010"; + public const string InvalidSendOnlyEndpointMethod = "NSBFUNC011"; internal static readonly DiagnosticDescriptor ClassMustBePartialDescriptor = new( id: ClassMustBePartial, @@ -98,4 +99,12 @@ static class DiagnosticIds category: "NServiceBus.AzureFunctions", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor InvalidSendOnlyEndpointMethodDescriptor = new( + id: InvalidSendOnlyEndpointMethod, + title: "Invalid NServiceBus send-only endpoint method", + messageFormat: "Method '{0}' is not a valid NServiceBus send-only endpoint: {1}", + category: "NServiceBus.AzureFunctions", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs index 3714702f..c494c2ae 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs @@ -39,6 +39,10 @@ public static void Emit(SourceProductionContext context, CompositionSpec? compos writer.WriteLine("{"); writer.WriteLine(" manifest.Register(builder, manifest);"); writer.WriteLine("}"); + writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetSendOnlyEndpointManifests())"); + writer.WriteLine("{"); + writer.WriteLine(" manifest.Register(builder, manifest);"); + writer.WriteLine("}"); } writer.CloseCurlies(); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs index fa449908..16052729 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs @@ -23,7 +23,7 @@ internal static HostProjectSpec ParseHostProject(AnalyzerConfigOptionsProvider p return new HostProjectSpec(isHostProject, effectiveRootNameSpace); } - internal static CompositionSpec? ParseComposition(Compilation compilation, HostProjectSpec hostProject, bool hasLocalFunctions, CancellationToken cancellationToken = default) + internal static CompositionSpec? ParseComposition(Compilation compilation, HostProjectSpec hostProject, bool hasLocalFunctions, bool hasLocalSendOnlyEndpoints, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -45,7 +45,7 @@ internal static HostProjectSpec ParseHostProject(AnalyzerConfigOptionsProvider p } } - if (hasLocalFunctions) + if (hasLocalFunctions || hasLocalSendOnlyEndpoints) { registrations.Add(CreateGeneratedRegistrationClassSpec(compilation.Assembly)); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.TrackingNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.TrackingNames.cs index d0867ffe..46764b34 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.TrackingNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.TrackingNames.cs @@ -6,8 +6,9 @@ internal static class TrackingNames { public const string HostProject = nameof(HostProject); public const string LocalFunctions = nameof(LocalFunctions); + public const string LocalSendOnlyEndpoints = nameof(LocalSendOnlyEndpoints); public const string Composition = nameof(Composition); - public static string[] All => [HostProject, LocalFunctions, Composition]; + public static string[] All => [HostProject, LocalFunctions, LocalSendOnlyEndpoints, Composition]; } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs index 769fb37d..3905b7db 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs @@ -21,10 +21,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (matches, _) => matches.Length > 0) .WithTrackingName(TrackingNames.LocalFunctions); + var hasLocalSendOnlyEndpoints = context.SyntaxProvider + .ForAttributeWithMetadataName( + KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, + static (node, _) => node is MethodDeclarationSyntax, + static (_, _) => true) + .Collect() + .Select(static (matches, _) => matches.Length > 0) + .WithTrackingName(TrackingNames.LocalSendOnlyEndpoints); + var compositions = context.CompilationProvider .Combine(hostProject) .Combine(hasLocalFunctions) - .Select(static (data, cancellationToken) => Parser.ParseComposition(data.Left.Left, data.Left.Right, data.Right, cancellationToken)) + .Combine(hasLocalSendOnlyEndpoints) + .Select(static (data, cancellationToken) => Parser.ParseComposition(data.Left.Left.Left, data.Left.Left.Right, data.Left.Right, data.Right, cancellationToken)) .WithTrackingName(TrackingNames.Composition); context.RegisterSourceOutput( diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs index a5ea280c..b4978e02 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs @@ -23,4 +23,7 @@ public sealed partial class FunctionEndpointGenerator MessageActions, ParameterRole.FunctionContext, ParameterRole.CancellationToken)); + + static readonly SendOnlyEndpointDefinition AzureServiceBusSendOnlyEndpoint = new( + RegistrationMethodFullyQualified: $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs index af9e880f..076c772e 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs @@ -10,15 +10,19 @@ public sealed partial class FunctionEndpointGenerator { static class Emitter { - public static void Emit(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + public static void Emit(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) { - if (functions.Length <= 0) + if (functions.Length <= 0 && sendOnlyEndpoints.Length <= 0) { return; } - EmitMethodBodies(spc, functions); - EmitRegistration(spc, functions, assemblyClassName); + if (functions.Length > 0) + { + EmitMethodBodies(spc, functions); + } + + EmitRegistration(spc, functions, sendOnlyEndpoints, assemblyClassName); } static void EmitMethodBodies(SourceProductionContext spc, ImmutableArray functions) @@ -75,7 +79,7 @@ static void EmitMethodBodies(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) + static void EmitRegistration(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) { var writer = new SourceWriter(); writer.PreAmble(); @@ -106,7 +110,31 @@ static void EmitRegistration(SourceProductionContext spc, ImmutableArray"); + writer.WriteLine("/// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly."); + writer.WriteLine("/// "); + writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); + writer.WriteLine(" GetSendOnlyEndpointManifests()"); + writer.WriteLine("{"); + writer.Indentation++; + + foreach (var endpoint in sendOnlyEndpoints.OrderBy(f => f.EndpointName, StringComparer.Ordinal)) + { + writer.WriteLine("yield return new global::NServiceBus.SendOnlyEndpointManifest("); + writer.WriteLine($" \"{endpoint.EndpointName}\","); + writer.WriteLine($" {GenerateConfigureMethodCall(endpoint.ConfigureMethod)},"); + writer.WriteLine($" {endpoint.RegistrationMethodFullyQualified});"); + } + + writer.WriteLine("yield break;"); + writer.Indentation--; + writer.WriteLine("}"); + writer.Indentation--; + writer.WriteLine("}"); spc.AddSource("FunctionRegistration.g.cs", writer.ToSourceText()); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs index 91d8dfbb..dfec1b91 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs @@ -34,6 +34,27 @@ internal static FunctionSpecs Extract(GeneratorAttributeSyntaxContext context, T }; } + internal static SendOnlyEndpointSpecs ExtractSendOnly(GeneratorAttributeSyntaxContext context, SendOnlyEndpointDefinition sendOnlyEndpointDefinition, CancellationToken cancellationToken = default) + { + if (context.Attributes.Length == 0) + { + return SendOnlyEndpointSpecs.Empty; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (!FunctionEndpointGeneratorKnownTypes.TryGet(context.SemanticModel.Compilation, AzureServiceBusTrigger, out var knownTypes)) + { + return SendOnlyEndpointSpecs.Empty; + } + + return context.TargetSymbol switch + { + IMethodSymbol methodSymbol => ExtractSendOnlyFromMethod(methodSymbol, knownTypes, sendOnlyEndpointDefinition), + _ => SendOnlyEndpointSpecs.Empty + }; + } + static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpointGeneratorKnownTypes knownTypes, TriggerDefinition triggerDefinition, CancellationToken cancellationToken) { var diagnostics = new List(); @@ -60,6 +81,16 @@ static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpo return new FunctionSpecs(functions, diagnostics.ToImmutableEquatableArray()); } + static SendOnlyEndpointSpecs ExtractSendOnlyFromMethod(IMethodSymbol methodSymbol, FunctionEndpointGeneratorKnownTypes knownTypes, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) + { + var diagnostics = new List(); + var spec = ExtractSendOnlyEndpointSpec(methodSymbol, knownTypes, sendOnlyEndpointDefinition, diagnostics); + var sendOnlyEndpoints = spec is null + ? ImmutableEquatableArray.Empty + : ((SendOnlyEndpointSpec[])[spec]).ToImmutableEquatableArray(); + return new SendOnlyEndpointSpecs(sendOnlyEndpoints, diagnostics.ToImmutableEquatableArray()); + } + static FunctionSpec? ExtractFunctionSpec(IMethodSymbol method, FunctionEndpointGeneratorKnownTypes knownTypes, TriggerDefinition triggerDefinition, List diagnostics) { if (!TryGetFunctionAttribute(method, knownTypes, out var functionAttr) || functionAttr.ConstructorArguments.Length == 0) @@ -280,6 +311,79 @@ static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpo triggerDefinition.ProcessorTypeFullyQualified, triggerDefinition.RegistrationMethodFullyQualified, processCallExpression, configureMethod!.Value); } + static SendOnlyEndpointSpec? ExtractSendOnlyEndpointSpec( + IMethodSymbol method, + FunctionEndpointGeneratorKnownTypes knownTypes, + SendOnlyEndpointDefinition sendOnlyEndpointDefinition, + List diagnostics) + { + if (!TryGetSendOnlyEndpointAttribute(method, knownTypes, out var sendOnlyEndpointAttribute) + || sendOnlyEndpointAttribute.ConstructorArguments.Length == 0 + || sendOnlyEndpointAttribute.ConstructorArguments[0].Value is not string endpointName) + { + return null; + } + + var problems = ImmutableList.CreateBuilder(); + + if (!method.IsStatic) + { + problems.Add("method must be static"); + } + + var expectedMethodName = $"Configure{endpointName}"; + if (!string.Equals(method.Name, expectedMethodName, StringComparison.OrdinalIgnoreCase)) + { + problems.Add($"method name must be '{expectedMethodName}'"); + } + + if (method.Parameters.Length == 0 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, knownTypes.EndpointConfiguration)) + { + problems.Add("first parameter must be EndpointConfiguration"); + } + + for (var i = 1; i < method.Parameters.Length; i++) + { + if (!IsAllowedConfigureMethodParameterType(method.Parameters[i].Type, knownTypes)) + { + problems.Add("parameters after EndpointConfiguration must be IServiceCollection, IConfiguration, or IHostEnvironment"); + break; + } + } + + if (problems.Count > 0) + { + diagnostics.Add(CreateDiagnostic(DiagnosticIds.InvalidSendOnlyEndpointMethodDescriptor, method, method.Name, string.Join(", ", problems))); + return null; + } + + var containingType = method.ContainingType; + var ns = containingType.ContainingNamespace.IsGlobalNamespace ? "" : containingType.ContainingNamespace.ToDisplayString(); + + var parameterTypeNames = new string[method.Parameters.Length]; + for (var i = 0; i < method.Parameters.Length; i++) + { + parameterTypeNames[i] = method.Parameters[i].Type.Name.ToLowerInvariant(); + } + + var configureMethod = new ConfigureMethodSpec( + method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + method.Name, + parameterTypeNames.ToImmutableEquatableArray()); + + return new SendOnlyEndpointSpec( + ns, + containingType.Name, + endpointName, + sendOnlyEndpointDefinition.RegistrationMethodFullyQualified, + configureMethod); + } + + static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, FunctionEndpointGeneratorKnownTypes knownTypes) + => SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IServiceCollection) + || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IConfiguration) + || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IHostEnvironment); + static ParameterRole? ClassifyParameterRole(IParameterSymbol parameter, bool hasTriggerAttribute, FunctionEndpointGeneratorKnownTypes knownTypes) { if (hasTriggerAttribute) @@ -544,6 +648,21 @@ static bool TryGetFunctionAttribute(IMethodSymbol method, FunctionEndpointGenera return false; } + static bool TryGetSendOnlyEndpointAttribute(IMethodSymbol method, FunctionEndpointGeneratorKnownTypes knownTypes, [NotNullWhen(true)] out AttributeData? sendOnlyEndpointAttribute) + { + foreach (var attribute in method.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, knownTypes.SendOnlyEndpointAttribute)) + { + sendOnlyEndpointAttribute = attribute; + return true; + } + } + + sendOnlyEndpointAttribute = null!; + return false; + } + static bool IsPartial(INamedTypeSymbol type, CancellationToken cancellationToken) { foreach (var syntaxReference in type.DeclaringSyntaxReferences) @@ -592,13 +711,28 @@ internal sealed record FunctionSpec( string ProcessCallExpression, ConfigureMethodSpec ConfigureMethod); + internal sealed record SendOnlyEndpointSpec( + string ContainingNamespace, + string ContainingClassName, + string EndpointName, + string RegistrationMethodFullyQualified, + ConfigureMethodSpec ConfigureMethod); + internal readonly record struct FunctionSpecs(ImmutableEquatableArray Functions, ImmutableEquatableArray Diagnostics) { public static FunctionSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); } + internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray SendOnlyEndpoints, ImmutableEquatableArray Diagnostics) + { + public static SendOnlyEndpointSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); + } + + internal readonly record struct SendOnlyEndpointDefinition(string RegistrationMethodFullyQualified); + readonly struct FunctionEndpointGeneratorKnownTypes( INamedTypeSymbol functionAttribute, + INamedTypeSymbol sendOnlyEndpointAttribute, INamedTypeSymbol triggerAttribute, INamedTypeSymbol functionContext, INamedTypeSymbol cancellationToken, @@ -610,6 +744,7 @@ readonly struct FunctionEndpointGeneratorKnownTypes( ImmutableDictionary additionalParameterSymbols) { public INamedTypeSymbol FunctionAttribute { get; } = functionAttribute; + public INamedTypeSymbol SendOnlyEndpointAttribute { get; } = sendOnlyEndpointAttribute; public INamedTypeSymbol TriggerAttribute { get; } = triggerAttribute; public INamedTypeSymbol FunctionContext { get; } = functionContext; public INamedTypeSymbol CancellationToken { get; } = cancellationToken; @@ -624,6 +759,7 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi { var functionAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.FunctionAttribute); + var sendOnlyEndpointAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute); var triggerAttribute = compilation.GetTypeByMetadataName(triggerDefinition.TriggerAttributeMetadataName); var functionContext = compilation.GetTypeByMetadataName(KnownTypeNames.FunctionContext); @@ -635,6 +771,7 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi var iHostEnvironment = compilation.GetTypeByMetadataName(KnownTypeNames.IHostEnvironment); if (functionAttribute is null + || sendOnlyEndpointAttribute is null || triggerAttribute is null || functionContext is null || cancellationToken is null @@ -662,6 +799,7 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi knownTypes = new FunctionEndpointGeneratorKnownTypes( functionAttribute, + sendOnlyEndpointAttribute, triggerAttribute, functionContext, cancellationToken, diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs index a5ffe27e..7c32cabc 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs @@ -5,11 +5,13 @@ public sealed partial class FunctionEndpointGenerator internal static class TrackingNames { public const string Extraction = nameof(Extraction); + public const string SendOnlyExtraction = nameof(SendOnlyExtraction); public const string Diagnostics = nameof(Diagnostics); public const string Functions = nameof(Functions); + public const string SendOnlyEndpoints = nameof(SendOnlyEndpoints); public const string AssemblyClassName = nameof(AssemblyClassName); public const string Combined = nameof(Combined); - public static string[] All => [Extraction, Diagnostics, Functions, AssemblyClassName, Combined]; + public static string[] All => [Extraction, SendOnlyExtraction, Diagnostics, Functions, SendOnlyEndpoints, AssemblyClassName, Combined]; } -} \ No newline at end of file +} diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 18bcdccc..2de5b779 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -8,9 +8,9 @@ namespace NServiceBus.AzureFunctions.Analyzer; public sealed partial class FunctionEndpointGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) - => InitializeGenerator(context, AzureServiceBusTrigger); + => InitializeGenerator(context, AzureServiceBusTrigger, AzureServiceBusSendOnlyEndpoint); - internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition) + internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) { var extractionCandidates = context.SyntaxProvider .ForAttributeWithMetadataName( @@ -18,15 +18,28 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); + var sendOnlyExtractionCandidates = context.SyntaxProvider + .ForAttributeWithMetadataName( + KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, + predicate: static (node, _) => node is MethodDeclarationSyntax, + transform: static (ctx, _) => ctx); + var triggerDefinitionProvider = CreateTriggerDefinitionProvider(context, triggerDefinition); + var sendOnlyEndpointDefinitionProvider = CreateSendOnlyEndpointDefinitionProvider(context, sendOnlyEndpointDefinition); var extractionResults = extractionCandidates .Combine(triggerDefinitionProvider) .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) .WithTrackingName(TrackingNames.Extraction); + var sendOnlyExtractionResults = sendOnlyExtractionCandidates + .Combine(sendOnlyEndpointDefinitionProvider) + .Select(static (pair, ct) => Parser.ExtractSendOnly(pair.Left, pair.Right, ct)) + .WithTrackingName(TrackingNames.SendOnlyExtraction); + var diagnostics = extractionResults - .Collect() // Materialize all results for cross-method diagnostic deduplication. + .Collect() + .Combine(sendOnlyExtractionResults.Collect()) .SelectMany(static (results, _) => { // DiagnosticWithInfo implements structural equality (Location, Info, AdditionalLocations) @@ -35,10 +48,15 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte // across steps. Within an edited file, new tree references cause re-reporting, which is // correct and cheap. var diagnostics = new HashSet(); - foreach (var result in results) + foreach (var result in results.Left) { diagnostics.UnionWith(result.Diagnostics); } + + foreach (var sendOnlyResult in results.Right) + { + diagnostics.UnionWith(sendOnlyResult.Diagnostics); + } return diagnostics.ToImmutableEquatableArray(); }) .WithTrackingName(TrackingNames.Diagnostics); @@ -50,19 +68,29 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte .SelectMany(static (result, _) => result.Functions) .WithTrackingName(TrackingNames.Functions); + var sendOnlyEndpointSpecs = sendOnlyExtractionResults + .SelectMany(static (result, _) => result.SendOnlyEndpoints) + .WithTrackingName(TrackingNames.SendOnlyEndpoints); + var assemblyClassName = context.CompilationProvider .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()) .WithTrackingName(TrackingNames.AssemblyClassName); var combined = functionSpecs.Collect() + .Combine(sendOnlyEndpointSpecs.Collect()) .Combine(assemblyClassName) .WithTrackingName(TrackingNames.Combined); - context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left, data.Right)); + context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left.Left, data.Left.Right, data.Right)); static IncrementalValueProvider CreateTriggerDefinitionProvider( IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition) => context.CompilationProvider.Select((_, _) => triggerDefinition); + + static IncrementalValueProvider CreateSendOnlyEndpointDefinitionProvider( + IncrementalGeneratorInitializationContext context, + SendOnlyEndpointDefinition sendOnlyEndpointDefinition) => + context.CompilationProvider.Select((_, _) => sendOnlyEndpointDefinition); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs index 8002f5a2..dfb97d02 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs @@ -5,6 +5,7 @@ static class KnownTypeNames public const string FunctionAttribute = "Microsoft.Azure.Functions.Worker.FunctionAttribute"; public const string FunctionContext = "Microsoft.Azure.Functions.Worker.FunctionContext"; public const string NServiceBusFunctionAttribute = "NServiceBus.NServiceBusFunctionAttribute"; + public const string NServiceBusSendOnlyEndpointAttribute = "NServiceBus.NServiceBusSendOnlyEndpointAttribute"; public const string CancellationToken = "System.Threading.CancellationToken"; public const string EndpointConfigurationType = "NServiceBus.EndpointConfiguration"; public const string IHandleMessages = "NServiceBus.IHandleMessages`1"; @@ -16,9 +17,9 @@ static class KnownTypeNames public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; public const string IConfiguration = "Microsoft.Extensions.Configuration.IConfiguration"; public const string IHostEnvironment = "Microsoft.Extensions.Hosting.IHostEnvironment"; - public const string FunctionsHostApplicationBuilderExtensions = "NServiceBus.FunctionsHostApplicationBuilderExtensions"; public const string AzureServiceBusFunctionsHostApplicationBuilderExtensions = "NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions"; - public const string AddSendOnlyNServiceBusEndpoint = "AddSendOnlyNServiceBusEndpoint"; + public const string SendOnlyEndpointManifest = "NServiceBus.SendOnlyEndpointManifest"; public const string AddNServiceBusFunctions = "AddNServiceBusFunctions"; public const string AddNServiceBusAzureServiceBusFunction = "AddNServiceBusAzureServiceBusFunction"; + public const string AddNServiceBusAzureServiceBusSendOnlyEndpoint = "AddNServiceBusAzureServiceBusSendOnlyEndpoint"; } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs index 1ba1324f..c3b000d4 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs @@ -21,7 +21,7 @@ public static void AddNServiceBusAzureServiceBusFunction(this FunctionsApplicati { builder.Services.AddAzureClientsCore(); - var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest, nameof(FunctionsHostApplicationBuilderExtensions.AddSendOnlyNServiceBusEndpoint)); + var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest, $"[{nameof(NServiceBusSendOnlyEndpointAttribute)}]"); var transport = GetAzureServiceBusTransport(endpointConfiguration); var resolvedConnectionSettingName = string.IsNullOrWhiteSpace(functionManifest.ConnectionSettingName) @@ -32,6 +32,26 @@ public static void AddNServiceBusAzureServiceBusFunction(this FunctionsApplicati builder.Services.AddKeyedSingleton(functionManifest.Name, (_, _) => new AzureServiceBusMessageProcessor(transport, functionManifest.Name)); } + /// + /// Adds the necessary services to the container to support the send-only endpoint described by the provided . + /// Should only be called by the source generator. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void AddNServiceBusAzureServiceBusSendOnlyEndpoint(this FunctionsApplicationBuilder builder, SendOnlyEndpointManifest sendOnlyEndpointManifest) + { + builder.Services.AddAzureClientsCore(); + + var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildSendOnlyEndpointConfiguration(builder, sendOnlyEndpointManifest); + var transport = GetAzureServiceBusTransport(endpointConfiguration); + + if (!string.IsNullOrWhiteSpace(transport.ConnectionName)) + { + transport.ConnectionName = FunctionBindingExpression.Resolve(transport.ConnectionName, builder.Configuration); + } + + builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointConfiguration.EndpointName); + } + static AzureServiceBusServerlessTransport GetAzureServiceBusTransport(EndpointConfiguration endpointConfiguration) { var transport = endpointConfiguration.GetSettings().GetOrDefault() as AzureServiceBusServerlessTransport; diff --git a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs index b871db45..70c07db2 100644 --- a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs +++ b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs @@ -37,7 +37,7 @@ public static EndpointConfiguration BuildReceiveEndpointConfiguration( if (endpointConfiguration.IsSendOnly) { - throw new InvalidOperationException($"Functions can't be send only endpoints, use {sendOnlyEndpointApiName}"); + throw new InvalidOperationException($"Functions can't be send-only endpoints, use {sendOnlyEndpointApiName}."); } var resolvedAddress = FunctionBindingExpression.Resolve(functionManifest.Address, builder.Configuration); @@ -50,22 +50,21 @@ public static EndpointConfiguration BuildReceiveEndpointConfiguration( } /// - /// Builds a send-only with the customizations supplied via - /// . + /// Builds a send-only from the supplied . /// /// The Functions application builder the endpoint is attached to. - /// The logical name of the send-only endpoint. - /// Callback invoked to configure the endpoint and register endpoint-specific services. + /// The manifest describing the send-only endpoint to configure. public static EndpointConfiguration BuildSendOnlyEndpointConfiguration( FunctionsApplicationBuilder builder, - string endpointName, - Action configure) + SendOnlyEndpointManifest manifest) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(endpointName); - ArgumentNullException.ThrowIfNull(configure); + ArgumentNullException.ThrowIfNull(manifest); - var endpointConfiguration = CreateDefaultEndpointConfiguration(endpointName, builder, configure); + var endpointConfiguration = CreateDefaultEndpointConfiguration( + manifest.Name, + builder, + (configuration, endpointServices) => manifest.Configuration(configuration, endpointServices, builder.Configuration, builder.Environment)); endpointConfiguration.SendOnly(); diff --git a/src/NServiceBus.AzureFunctions.Common/FunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.Common/FunctionsHostApplicationBuilderExtensions.cs deleted file mode 100644 index 23bdb292..00000000 --- a/src/NServiceBus.AzureFunctions.Common/FunctionsHostApplicationBuilderExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace NServiceBus; - -using System; -using Microsoft.Azure.Functions.Worker.Builder; -using Microsoft.Extensions.DependencyInjection; - -/// -/// Extensions methods to configure the FunctionsApplicationBuilder with NServiceBus endpoints. -/// -public static class FunctionsHostApplicationBuilderExtensions -{ - /// The functions application builder. - extension(FunctionsApplicationBuilder builder) - { - /// - /// Adds an NServiceBus endpoint to the Azure Functions host. The endpoint will be configured as send-only. - /// - /// It is possible to use any transport as send-only with this method but only the Serverless variants like AzureServiceBusServerlessTransport will provide first class - /// functions host integration support. - /// The endpoint name. - /// The configuration action to configure the endpoint configuration. - public void AddSendOnlyNServiceBusEndpoint(string endpointName, - Action configure) => builder.AddSendOnlyNServiceBusEndpoint(endpointName, (endpointConfiguration, _) => configure(endpointConfiguration)); - - /// - /// Adds an NServiceBus endpoint to the Azure Functions host. The endpoint will be configured as send-only. - /// - /// It is possible to use any transport as send-only with this method but only the Serverless variants like AzureServiceBusServerlessTransport will provide first class - /// functions host integration support. - /// The endpoint name. - /// The configuration action to configure the endpoint configuration and the endpoint-specific services, if any. - public void AddSendOnlyNServiceBusEndpoint(string endpointName, - Action configure) - { - var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildSendOnlyEndpointConfiguration(builder, endpointName, configure); - - builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointName); - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt index dc8730e6..0b819f6d 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt @@ -29,5 +29,6 @@ namespace NServiceBus.Configuration.AdvancedExtensibility public static class AzureServiceBusFunctionsHostApplicationBuilderExtensions { public static void AddNServiceBusAzureServiceBusFunction(this Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.FunctionManifest functionManifest) { } + public static void AddNServiceBusAzureServiceBusSendOnlyEndpoint(this Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.SendOnlyEndpointManifest sendOnlyEndpointManifest) { } } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt index 59950049..efd0be32 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt @@ -13,7 +13,7 @@ namespace NServiceBus public static class FunctionEndpointConfigurationBuilder { public static NServiceBus.EndpointConfiguration BuildReceiveEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.FunctionManifest functionManifest, string sendOnlyEndpointApiName) { } - public static NServiceBus.EndpointConfiguration BuildSendOnlyEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, string endpointName, System.Action configure) { } + public static NServiceBus.EndpointConfiguration BuildSendOnlyEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.SendOnlyEndpointManifest manifest) { } } public sealed class FunctionManifest : System.IEquatable { @@ -24,19 +24,24 @@ namespace NServiceBus public string Name { get; init; } public System.Action Register { get; init; } } - public static class FunctionsHostApplicationBuilderExtensions - { - extension(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder) - { - public void AddSendOnlyNServiceBusEndpoint(string endpointName, System.Action configure) { } - public void AddSendOnlyNServiceBusEndpoint(string endpointName, System.Action configure) { } - } - } [System.AttributeUsage(System.AttributeTargets.Method, Inherited=false)] public sealed class NServiceBusFunctionAttribute : System.Attribute { public NServiceBusFunctionAttribute() { } } + [System.AttributeUsage(System.AttributeTargets.Method, Inherited=false)] + public sealed class NServiceBusSendOnlyEndpointAttribute : System.Attribute + { + public NServiceBusSendOnlyEndpointAttribute(string endpointName) { } + public string EndpointName { get; } + } + public sealed class SendOnlyEndpointManifest : System.IEquatable + { + public SendOnlyEndpointManifest(string Name, NServiceBus.FunctionEndpointConfiguration Configuration, System.Action Register) { } + public NServiceBus.FunctionEndpointConfiguration Configuration { get; init; } + public string Name { get; init; } + public System.Action Register { get; init; } + } public sealed class ServerlessTransportInfrastructure : NServiceBus.Transport.TransportInfrastructure { public ServerlessTransportInfrastructure(NServiceBus.Transport.TransportInfrastructure baseTransportInfrastructure, System.Func messageReceiverFactory) { } diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt index d5dc2f21..980b998b 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt @@ -52,6 +52,10 @@ internal static class NServiceBusFunctionsComposition { manifest.Register(builder, manifest); } + foreach (var manifest in global::NServiceBus.Generated.GeneratedFunctionRegistrations_GeneratesProjectComposition_4d91953014478208.GetSendOnlyEndpointManifests()) + { + manifest.Register(builder, manifest); + } } } } @@ -116,4 +120,13 @@ public static class GeneratedFunctionRegistrations_GeneratesProjectComposition_4 global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield break; + } } \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt index 77d98b5d..70f06b30 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt @@ -63,4 +63,13 @@ public static class GeneratedFunctionRegistrations_GeneratesEndpointWithExtraUnr global::Demo.Testing.TestFunctionManifestRegistration.Register); yield break; } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield break; + } } \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt index 2557dd15..ce7c4978 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt @@ -62,4 +62,13 @@ public static class GeneratedFunctionRegistrations_GeneratesEndpointWithoutMessa global::Demo.Testing.TestFunctionManifestRegistration.Register); yield break; } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield break; + } } \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt index 739ef546..4e3bb557 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt @@ -58,4 +58,13 @@ public static class GeneratedFunctionRegistrations_GeneratesFunctionEndpoint_64a global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield break; + } } \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt index ed736d3f..74af0b02 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt @@ -55,4 +55,13 @@ public static class GeneratedFunctionRegistrations_GeneratesFunctionEndpointInGl global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield break; + } } \ No newline at end of file diff --git a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs index 57c56dd8..0e6653f2 100644 --- a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs +++ b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs @@ -120,19 +120,16 @@ public void Send({{optionsType}} options) public Task DoesNotReportUseTransportWithAzureServiceBusServerlessTransport() { var source = """ - using Microsoft.Azure.Functions.Worker.Builder; using NServiceBus.AzureFunctions.AzureServiceBus; using NServiceBus.Transport.AzureServiceBus; namespace Demo; - public static class Program + public static class ClientEndpoint { - public static void Configure(FunctionsApplicationBuilder builder) + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration) { - builder.AddSendOnlyNServiceBusEndpoint("client", (endpointConfiguration, services) => - { - endpointConfiguration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); - }); + endpointConfiguration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); } } """; @@ -165,17 +162,14 @@ public static void Apply(EndpointConfiguration endpointConfiguration) public Task ReportsDiagnosticForUnsupportedEndpointConfigurationCallsInSendOnlyCallbacks(string configuration, string diagnosticId) { var source = $$""" - using Microsoft.Azure.Functions.Worker.Builder; namespace Demo; - public static class Program + public static class ClientEndpoint { - public static void Configure(FunctionsApplicationBuilder builder) + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration) { - builder.AddSendOnlyNServiceBusEndpoint("client", (endpointConfiguration, services) => - { - [|endpointConfiguration.{{configuration}}|]; - }); + [|endpointConfiguration.{{configuration}}|]; } } """; @@ -187,18 +181,13 @@ public static void Configure(FunctionsApplicationBuilder builder) public Task ReportsDiagnosticForUnsupportedEndpointConfigurationCallsInSendOnlyMethodGroupCallback() { var source = """ - using Microsoft.Azure.Functions.Worker.Builder; using Microsoft.Extensions.DependencyInjection; namespace Demo; - public static class Program + public static class ClientEndpoint { - public static void Configure(FunctionsApplicationBuilder builder) - { - builder.AddSendOnlyNServiceBusEndpoint("client", Program.ConfigureSendOnly); - } - - static void ConfigureSendOnly(EndpointConfiguration endpointConfiguration, IServiceCollection services) + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { [|endpointConfiguration.MakeInstanceUniquelyAddressable("instance")|]; } diff --git a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs index e739a2af..54d5d3ed 100644 --- a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs @@ -36,6 +36,48 @@ public void GeneratesEndpointWithoutMessageActions() => .Run() .Approve(); + [Test] + public void GeneratesSendOnlyEndpointRegistration() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidFunction) + .WithSource(""" + namespace Demo; + + using Microsoft.Extensions.DependencyInjection; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + } + } + """, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() + { + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(""" + namespace Demo; + + public class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public void ConfigureClient(EndpointConfiguration endpointConfiguration) + { + } + } + """) + .SuppressCompilationErrors() + .SuppressDiagnosticErrors() + .Run(); + + Assert.That(result.GeneratorDiagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod)); + } + [Test] public void ReportsInvalidFunctionMethodWhenShapeContainsExtraUnrecognizedParameters() { @@ -573,6 +615,11 @@ public static class TestFunctionManifestRegistration public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.FunctionManifest __) { } } + public static class TestSendOnlyEndpointManifestRegistration + { + public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.SendOnlyEndpointManifest __) { } + } + {{classBody}} """; } diff --git a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs index f8f3c371..6b82159a 100644 --- a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs @@ -24,7 +24,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Shape: FunctionEndpointGenerator.TriggerShape.RequiredAllowingAdditionalParameters( FunctionEndpointGenerator.ParameterRole.TriggerMessage, FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken))); + FunctionEndpointGenerator.ParameterRole.CancellationToken)), + new FunctionEndpointGenerator.SendOnlyEndpointDefinition("global::Demo.Testing.TestSendOnlyEndpointManifestRegistration.Register")); internal static class TrackingNames { diff --git a/src/Tests.Analyzers/NoMessageActionsGenerator.cs b/src/Tests.Analyzers/NoMessageActionsGenerator.cs index 6bebefff..1515b7b4 100644 --- a/src/Tests.Analyzers/NoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/NoMessageActionsGenerator.cs @@ -24,7 +24,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Shape: FunctionEndpointGenerator.TriggerShape.Required( FunctionEndpointGenerator.ParameterRole.TriggerMessage, FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken))); + FunctionEndpointGenerator.ParameterRole.CancellationToken)), + new FunctionEndpointGenerator.SendOnlyEndpointDefinition("global::Demo.Testing.TestSendOnlyEndpointManifestRegistration.Register")); internal static class TrackingNames { diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index ab150844..a975d55c 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -59,6 +59,11 @@ public static class TestFunctionManifestRegistration public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.FunctionManifest __) { } } + public static class TestSendOnlyEndpointManifestRegistration + { + public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.SendOnlyEndpointManifest __) { } + } + public partial class Functions { [NServiceBusFunction] From 58580566a21ccefda1c14cbe79e6a40a97a5e8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 09:52:16 +0200 Subject: [PATCH 02/25] Fixup --- src/IntegrationTestApp/ClientEndpoint.cs | 20 +++++ .../NServiceBusSendOnlyEndpointAttribute.cs | 14 ++++ .../SendOnlyEndpointManifest.cs | 17 +++++ ...sSendOnlyEndpointRegistration.approved.txt | 74 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 src/IntegrationTestApp/ClientEndpoint.cs create mode 100644 src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs create mode 100644 src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs create mode 100644 src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt diff --git a/src/IntegrationTestApp/ClientEndpoint.cs b/src/IntegrationTestApp/ClientEndpoint.cs new file mode 100644 index 00000000..47b4b976 --- /dev/null +++ b/src/IntegrationTestApp/ClientEndpoint.cs @@ -0,0 +1,20 @@ +namespace IntegrationTestApp; + +using IntegrationTest.Shared; +using Microsoft.Extensions.DependencyInjection; + +public static class ClientEndpoint +{ + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + services.AddSingleton(new MyComponent("client")); + + var transport = new AzureServiceBusServerlessTransport(TopicTopology.Default); + + var routing = endpointConfiguration.UseTransport(transport); + + routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); + endpointConfiguration.UseSerialization(); + } +} diff --git a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs new file mode 100644 index 00000000..55ff2b3f --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs @@ -0,0 +1,14 @@ +namespace NServiceBus; + +/// +/// Marks a static configuration method as the source of a send-only NServiceBus endpoint hosted in Azure Functions. +/// The source generator produces endpoint registration code from methods marked with this attribute. +/// +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +public sealed class NServiceBusSendOnlyEndpointAttribute(string endpointName) : Attribute +{ + /// + /// Gets the logical endpoint name. + /// + public string EndpointName { get; } = endpointName; +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs b/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs new file mode 100644 index 00000000..9052c485 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs @@ -0,0 +1,17 @@ +namespace NServiceBus; + +using Microsoft.Azure.Functions.Worker.Builder; + +/// +/// Describes a send-only NServiceBus endpoint hosted in Azure Functions. Produced by the source generator +/// and passed to the transport-specific registration method referenced by . +/// Should only be created by the source generator. +/// +/// The API surface might be changed between versions according to the needs of the source generator. +/// The endpoint name. +/// Callback invoked to customize the endpoint configuration and its service registrations. +/// Transport-specific callback that registers the endpoint with the Functions host builder. +public sealed record SendOnlyEndpointManifest( + string Name, + FunctionEndpointConfiguration Configuration, + Action Register); \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt new file mode 100644 index 00000000..2ace02f4 --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt @@ -0,0 +1,74 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.FunctionEndpointGenerator/FunctionMethodBodies.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +using Microsoft.Extensions.DependencyInjection; + +namespace Demo +{ + public partial class Functions + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public partial global::System.Threading.Tasks.Task Run( + global::Azure.Messaging.ServiceBus.ServiceBusReceivedMessage message, + global::Microsoft.Azure.Functions.Worker.ServiceBusMessageActions messageActions, + global::Microsoft.Azure.Functions.Worker.FunctionContext context, + global::System.Threading.CancellationToken cancellationToken) + { + var processor = context.InstanceServices + .GetRequiredKeyedService("ProcessOrder"); + return processor.Process(message, messageActions, context, cancellationToken); + } + } +} + +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.FunctionEndpointGenerator/FunctionRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus functions in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedFunctionRegistrations_GeneratesSendOnlyEndpointRegistration_12baa95a3a042aa3 +{ + /// + /// Gets function manifests for NServiceBus functions in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetFunctionManifests() + { + yield return new global::NServiceBus.FunctionManifest( + "ProcessOrder", "sales-queue", "AzureServiceBus", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.Functions.ConfigureProcessOrder(endpointconfiguration, iconfiguration, ihostenvironment), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); + yield break; + } + + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file From 12c835bca08f4dc4eb9bfafe325c4903385bc583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 10:16:41 +0200 Subject: [PATCH 03/25] Add separate SendOnlyEndpointGenerator --- ...nctionEndpointGenerator.AzureServiceBus.cs | 3 - .../FunctionEndpointGenerator.Emitter.cs | 26 +-- .../FunctionEndpointGenerator.Parser.cs | 140 +------------ ...FunctionEndpointGenerator.TrackingNames.cs | 6 +- .../FunctionEndpointGenerator.cs | 36 +--- ...ndOnlyEndpointGenerator.AzureServiceBus.cs | 7 + .../SendOnlyEndpointGenerator.Emitter.cs | 73 +++++++ .../SendOnlyEndpointGenerator.Parser.cs | 187 ++++++++++++++++++ ...SendOnlyEndpointGenerator.TrackingNames.cs | 15 ++ .../SendOnlyEndpointGenerator.cs | 58 ++++++ ...sSendOnlyEndpointRegistration.approved.txt | 37 +--- .../FunctionCompositionGeneratorTests.cs | 1 + .../FunctionEndpointGeneratorTests.cs | 19 +- .../LenientNoMessageActionsGenerator.cs | 3 +- .../MissingCompositionCallAnalyzerTests.cs | 1 + .../NoMessageActionsGenerator.cs | 3 +- 16 files changed, 374 insertions(+), 241 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.TrackingNames.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs index b4978e02..a5ea280c 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs @@ -23,7 +23,4 @@ public sealed partial class FunctionEndpointGenerator MessageActions, ParameterRole.FunctionContext, ParameterRole.CancellationToken)); - - static readonly SendOnlyEndpointDefinition AzureServiceBusSendOnlyEndpoint = new( - RegistrationMethodFullyQualified: $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs index 076c772e..e44fe4d6 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs @@ -10,19 +10,15 @@ public sealed partial class FunctionEndpointGenerator { static class Emitter { - public static void Emit(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) + public static void Emit(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) { - if (functions.Length <= 0 && sendOnlyEndpoints.Length <= 0) + if (functions.Length <= 0) { return; } - if (functions.Length > 0) - { - EmitMethodBodies(spc, functions); - } - - EmitRegistration(spc, functions, sendOnlyEndpoints, assemblyClassName); + EmitMethodBodies(spc, functions); + EmitRegistration(spc, functions, assemblyClassName); } static void EmitMethodBodies(SourceProductionContext spc, ImmutableArray functions) @@ -79,7 +75,7 @@ static void EmitMethodBodies(SourceProductionContext spc, ImmutableArray functions, ImmutableArray sendOnlyEndpoints, string assemblyClassName) + static void EmitRegistration(SourceProductionContext spc, ImmutableArray functions, string assemblyClassName) { var writer = new SourceWriter(); writer.PreAmble(); @@ -112,7 +108,6 @@ static void EmitRegistration(SourceProductionContext spc, ImmutableArray"); writer.WriteLine("/// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly."); @@ -121,15 +116,6 @@ static void EmitRegistration(SourceProductionContext spc, ImmutableArray f.EndpointName, StringComparer.Ordinal)) - { - writer.WriteLine("yield return new global::NServiceBus.SendOnlyEndpointManifest("); - writer.WriteLine($" \"{endpoint.EndpointName}\","); - writer.WriteLine($" {GenerateConfigureMethodCall(endpoint.ConfigureMethod)},"); - writer.WriteLine($" {endpoint.RegistrationMethodFullyQualified});"); - } - writer.WriteLine("yield break;"); writer.Indentation--; writer.WriteLine("}"); @@ -145,4 +131,4 @@ static string GenerateConfigureMethodCall(ConfigureMethodSpec configureMethod) return $"(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => {configureMethod.ContainingTypeFullyQualified}.{configureMethod.MethodName}({argumentList})"; } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs index dfec1b91..25c7d7a0 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs @@ -34,27 +34,6 @@ internal static FunctionSpecs Extract(GeneratorAttributeSyntaxContext context, T }; } - internal static SendOnlyEndpointSpecs ExtractSendOnly(GeneratorAttributeSyntaxContext context, SendOnlyEndpointDefinition sendOnlyEndpointDefinition, CancellationToken cancellationToken = default) - { - if (context.Attributes.Length == 0) - { - return SendOnlyEndpointSpecs.Empty; - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (!FunctionEndpointGeneratorKnownTypes.TryGet(context.SemanticModel.Compilation, AzureServiceBusTrigger, out var knownTypes)) - { - return SendOnlyEndpointSpecs.Empty; - } - - return context.TargetSymbol switch - { - IMethodSymbol methodSymbol => ExtractSendOnlyFromMethod(methodSymbol, knownTypes, sendOnlyEndpointDefinition), - _ => SendOnlyEndpointSpecs.Empty - }; - } - static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpointGeneratorKnownTypes knownTypes, TriggerDefinition triggerDefinition, CancellationToken cancellationToken) { var diagnostics = new List(); @@ -81,16 +60,6 @@ static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpo return new FunctionSpecs(functions, diagnostics.ToImmutableEquatableArray()); } - static SendOnlyEndpointSpecs ExtractSendOnlyFromMethod(IMethodSymbol methodSymbol, FunctionEndpointGeneratorKnownTypes knownTypes, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) - { - var diagnostics = new List(); - var spec = ExtractSendOnlyEndpointSpec(methodSymbol, knownTypes, sendOnlyEndpointDefinition, diagnostics); - var sendOnlyEndpoints = spec is null - ? ImmutableEquatableArray.Empty - : ((SendOnlyEndpointSpec[])[spec]).ToImmutableEquatableArray(); - return new SendOnlyEndpointSpecs(sendOnlyEndpoints, diagnostics.ToImmutableEquatableArray()); - } - static FunctionSpec? ExtractFunctionSpec(IMethodSymbol method, FunctionEndpointGeneratorKnownTypes knownTypes, TriggerDefinition triggerDefinition, List diagnostics) { if (!TryGetFunctionAttribute(method, knownTypes, out var functionAttr) || functionAttr.ConstructorArguments.Length == 0) @@ -311,79 +280,6 @@ static SendOnlyEndpointSpecs ExtractSendOnlyFromMethod(IMethodSymbol methodSymbo triggerDefinition.ProcessorTypeFullyQualified, triggerDefinition.RegistrationMethodFullyQualified, processCallExpression, configureMethod!.Value); } - static SendOnlyEndpointSpec? ExtractSendOnlyEndpointSpec( - IMethodSymbol method, - FunctionEndpointGeneratorKnownTypes knownTypes, - SendOnlyEndpointDefinition sendOnlyEndpointDefinition, - List diagnostics) - { - if (!TryGetSendOnlyEndpointAttribute(method, knownTypes, out var sendOnlyEndpointAttribute) - || sendOnlyEndpointAttribute.ConstructorArguments.Length == 0 - || sendOnlyEndpointAttribute.ConstructorArguments[0].Value is not string endpointName) - { - return null; - } - - var problems = ImmutableList.CreateBuilder(); - - if (!method.IsStatic) - { - problems.Add("method must be static"); - } - - var expectedMethodName = $"Configure{endpointName}"; - if (!string.Equals(method.Name, expectedMethodName, StringComparison.OrdinalIgnoreCase)) - { - problems.Add($"method name must be '{expectedMethodName}'"); - } - - if (method.Parameters.Length == 0 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, knownTypes.EndpointConfiguration)) - { - problems.Add("first parameter must be EndpointConfiguration"); - } - - for (var i = 1; i < method.Parameters.Length; i++) - { - if (!IsAllowedConfigureMethodParameterType(method.Parameters[i].Type, knownTypes)) - { - problems.Add("parameters after EndpointConfiguration must be IServiceCollection, IConfiguration, or IHostEnvironment"); - break; - } - } - - if (problems.Count > 0) - { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.InvalidSendOnlyEndpointMethodDescriptor, method, method.Name, string.Join(", ", problems))); - return null; - } - - var containingType = method.ContainingType; - var ns = containingType.ContainingNamespace.IsGlobalNamespace ? "" : containingType.ContainingNamespace.ToDisplayString(); - - var parameterTypeNames = new string[method.Parameters.Length]; - for (var i = 0; i < method.Parameters.Length; i++) - { - parameterTypeNames[i] = method.Parameters[i].Type.Name.ToLowerInvariant(); - } - - var configureMethod = new ConfigureMethodSpec( - method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - method.Name, - parameterTypeNames.ToImmutableEquatableArray()); - - return new SendOnlyEndpointSpec( - ns, - containingType.Name, - endpointName, - sendOnlyEndpointDefinition.RegistrationMethodFullyQualified, - configureMethod); - } - - static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, FunctionEndpointGeneratorKnownTypes knownTypes) - => SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IServiceCollection) - || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IConfiguration) - || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IHostEnvironment); - static ParameterRole? ClassifyParameterRole(IParameterSymbol parameter, bool hasTriggerAttribute, FunctionEndpointGeneratorKnownTypes knownTypes) { if (hasTriggerAttribute) @@ -648,21 +544,6 @@ static bool TryGetFunctionAttribute(IMethodSymbol method, FunctionEndpointGenera return false; } - static bool TryGetSendOnlyEndpointAttribute(IMethodSymbol method, FunctionEndpointGeneratorKnownTypes knownTypes, [NotNullWhen(true)] out AttributeData? sendOnlyEndpointAttribute) - { - foreach (var attribute in method.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, knownTypes.SendOnlyEndpointAttribute)) - { - sendOnlyEndpointAttribute = attribute; - return true; - } - } - - sendOnlyEndpointAttribute = null!; - return false; - } - static bool IsPartial(INamedTypeSymbol type, CancellationToken cancellationToken) { foreach (var syntaxReference in type.DeclaringSyntaxReferences) @@ -711,28 +592,13 @@ internal sealed record FunctionSpec( string ProcessCallExpression, ConfigureMethodSpec ConfigureMethod); - internal sealed record SendOnlyEndpointSpec( - string ContainingNamespace, - string ContainingClassName, - string EndpointName, - string RegistrationMethodFullyQualified, - ConfigureMethodSpec ConfigureMethod); - internal readonly record struct FunctionSpecs(ImmutableEquatableArray Functions, ImmutableEquatableArray Diagnostics) { public static FunctionSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); } - internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray SendOnlyEndpoints, ImmutableEquatableArray Diagnostics) - { - public static SendOnlyEndpointSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); - } - - internal readonly record struct SendOnlyEndpointDefinition(string RegistrationMethodFullyQualified); - readonly struct FunctionEndpointGeneratorKnownTypes( INamedTypeSymbol functionAttribute, - INamedTypeSymbol sendOnlyEndpointAttribute, INamedTypeSymbol triggerAttribute, INamedTypeSymbol functionContext, INamedTypeSymbol cancellationToken, @@ -744,7 +610,6 @@ readonly struct FunctionEndpointGeneratorKnownTypes( ImmutableDictionary additionalParameterSymbols) { public INamedTypeSymbol FunctionAttribute { get; } = functionAttribute; - public INamedTypeSymbol SendOnlyEndpointAttribute { get; } = sendOnlyEndpointAttribute; public INamedTypeSymbol TriggerAttribute { get; } = triggerAttribute; public INamedTypeSymbol FunctionContext { get; } = functionContext; public INamedTypeSymbol CancellationToken { get; } = cancellationToken; @@ -759,7 +624,6 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi { var functionAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.FunctionAttribute); - var sendOnlyEndpointAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute); var triggerAttribute = compilation.GetTypeByMetadataName(triggerDefinition.TriggerAttributeMetadataName); var functionContext = compilation.GetTypeByMetadataName(KnownTypeNames.FunctionContext); @@ -771,7 +635,6 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi var iHostEnvironment = compilation.GetTypeByMetadataName(KnownTypeNames.IHostEnvironment); if (functionAttribute is null - || sendOnlyEndpointAttribute is null || triggerAttribute is null || functionContext is null || cancellationToken is null @@ -799,7 +662,6 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi knownTypes = new FunctionEndpointGeneratorKnownTypes( functionAttribute, - sendOnlyEndpointAttribute, triggerAttribute, functionContext, cancellationToken, @@ -813,4 +675,4 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi return true; } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs index 7c32cabc..6a372534 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs @@ -5,13 +5,9 @@ public sealed partial class FunctionEndpointGenerator internal static class TrackingNames { public const string Extraction = nameof(Extraction); - public const string SendOnlyExtraction = nameof(SendOnlyExtraction); public const string Diagnostics = nameof(Diagnostics); public const string Functions = nameof(Functions); - public const string SendOnlyEndpoints = nameof(SendOnlyEndpoints); public const string AssemblyClassName = nameof(AssemblyClassName); public const string Combined = nameof(Combined); - - public static string[] All => [Extraction, SendOnlyExtraction, Diagnostics, Functions, SendOnlyEndpoints, AssemblyClassName, Combined]; } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 2de5b779..dddad590 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -8,9 +8,9 @@ namespace NServiceBus.AzureFunctions.Analyzer; public sealed partial class FunctionEndpointGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) - => InitializeGenerator(context, AzureServiceBusTrigger, AzureServiceBusSendOnlyEndpoint); + => InitializeGenerator(context, AzureServiceBusTrigger); - internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) + internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition) { var extractionCandidates = context.SyntaxProvider .ForAttributeWithMetadataName( @@ -18,28 +18,15 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); - var sendOnlyExtractionCandidates = context.SyntaxProvider - .ForAttributeWithMetadataName( - KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, - predicate: static (node, _) => node is MethodDeclarationSyntax, - transform: static (ctx, _) => ctx); - var triggerDefinitionProvider = CreateTriggerDefinitionProvider(context, triggerDefinition); - var sendOnlyEndpointDefinitionProvider = CreateSendOnlyEndpointDefinitionProvider(context, sendOnlyEndpointDefinition); var extractionResults = extractionCandidates .Combine(triggerDefinitionProvider) .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) .WithTrackingName(TrackingNames.Extraction); - var sendOnlyExtractionResults = sendOnlyExtractionCandidates - .Combine(sendOnlyEndpointDefinitionProvider) - .Select(static (pair, ct) => Parser.ExtractSendOnly(pair.Left, pair.Right, ct)) - .WithTrackingName(TrackingNames.SendOnlyExtraction); - var diagnostics = extractionResults .Collect() - .Combine(sendOnlyExtractionResults.Collect()) .SelectMany(static (results, _) => { // DiagnosticWithInfo implements structural equality (Location, Info, AdditionalLocations) @@ -48,15 +35,10 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte // across steps. Within an edited file, new tree references cause re-reporting, which is // correct and cheap. var diagnostics = new HashSet(); - foreach (var result in results.Left) + foreach (var result in results) { diagnostics.UnionWith(result.Diagnostics); } - - foreach (var sendOnlyResult in results.Right) - { - diagnostics.UnionWith(sendOnlyResult.Diagnostics); - } return diagnostics.ToImmutableEquatableArray(); }) .WithTrackingName(TrackingNames.Diagnostics); @@ -68,29 +50,19 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte .SelectMany(static (result, _) => result.Functions) .WithTrackingName(TrackingNames.Functions); - var sendOnlyEndpointSpecs = sendOnlyExtractionResults - .SelectMany(static (result, _) => result.SendOnlyEndpoints) - .WithTrackingName(TrackingNames.SendOnlyEndpoints); - var assemblyClassName = context.CompilationProvider .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()) .WithTrackingName(TrackingNames.AssemblyClassName); var combined = functionSpecs.Collect() - .Combine(sendOnlyEndpointSpecs.Collect()) .Combine(assemblyClassName) .WithTrackingName(TrackingNames.Combined); - context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left.Left, data.Left.Right, data.Right)); + context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left, data.Right)); static IncrementalValueProvider CreateTriggerDefinitionProvider( IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition) => context.CompilationProvider.Select((_, _) => triggerDefinition); - - static IncrementalValueProvider CreateSendOnlyEndpointDefinitionProvider( - IncrementalGeneratorInitializationContext context, - SendOnlyEndpointDefinition sendOnlyEndpointDefinition) => - context.CompilationProvider.Select((_, _) => sendOnlyEndpointDefinition); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs new file mode 100644 index 00000000..10b74828 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs @@ -0,0 +1,7 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +public sealed partial class SendOnlyEndpointGenerator +{ + static readonly SendOnlyEndpointDefinition AzureServiceBusSendOnlyEndpoint = new( + RegistrationMethodFullyQualified: $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"); +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs new file mode 100644 index 00000000..e631eede --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs @@ -0,0 +1,73 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Utility; + +public sealed partial class SendOnlyEndpointGenerator +{ + static class Emitter + { + public static void Emit(SourceProductionContext spc, ImmutableArray sendOnlyEndpoints, string assemblyClassName) + { + if (sendOnlyEndpoints.Length <= 0) + { + return; + } + + var writer = new SourceWriter(); + writer.PreAmble(); + writer.WithFileScopedNamespace("NServiceBus.Generated"); + writer.WriteLine("/// "); + writer.WriteLine("/// Registrations for NServiceBus send-only endpoints in this assembly."); + writer.WriteLine("/// "); + writer.WriteLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + writer.WriteLine("[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute]"); + writer.WithGeneratedCodeAttribute(); + writer.WriteLine($"public static class {assemblyClassName}"); + writer.WriteLine("{"); + writer.Indentation++; + writer.WriteLine("/// "); + writer.WriteLine("/// Gets function manifests for NServiceBus functions in this assembly."); + writer.WriteLine("/// "); + writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); + writer.WriteLine(" GetFunctionManifests()"); + writer.WriteLine("{"); + writer.Indentation++; + writer.WriteLine("yield break;"); + writer.Indentation--; + writer.WriteLine("}"); + writer.WriteLine(); + writer.WriteLine("/// "); + writer.WriteLine("/// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly."); + writer.WriteLine("/// "); + writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); + writer.WriteLine(" GetSendOnlyEndpointManifests()"); + writer.WriteLine("{"); + writer.Indentation++; + + foreach (var endpoint in sendOnlyEndpoints.OrderBy(f => f.EndpointName, StringComparer.Ordinal)) + { + writer.WriteLine("yield return new global::NServiceBus.SendOnlyEndpointManifest("); + writer.WriteLine($" \"{endpoint.EndpointName}\","); + writer.WriteLine($" {GenerateConfigureMethodCall(endpoint.ConfigureMethod)},"); + writer.WriteLine($" {endpoint.RegistrationMethodFullyQualified});"); + } + + writer.WriteLine("yield break;"); + writer.Indentation--; + writer.WriteLine("}"); + writer.Indentation--; + writer.WriteLine("}"); + + spc.AddSource("SendOnlyEndpointRegistration.g.cs", writer.ToSourceText()); + } + + static string GenerateConfigureMethodCall(ConfigureMethodSpec configureMethod) + { + var argumentList = string.Join(", ", configureMethod.ParameterTypeNames); + return $"(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => {configureMethod.ContainingTypeFullyQualified}.{configureMethod.MethodName}({argumentList})"; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs new file mode 100644 index 00000000..ae084b2d --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -0,0 +1,187 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer; + +public sealed partial class SendOnlyEndpointGenerator +{ + static class Parser + { + internal static SendOnlyEndpointSpecs Extract(GeneratorAttributeSyntaxContext context, SendOnlyEndpointDefinition sendOnlyEndpointDefinition, CancellationToken cancellationToken = default) + { + if (context.Attributes.Length == 0) + { + return SendOnlyEndpointSpecs.Empty; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (!SendOnlyEndpointGeneratorKnownTypes.TryGet(context.SemanticModel.Compilation, out var knownTypes)) + { + return SendOnlyEndpointSpecs.Empty; + } + + return context.TargetSymbol switch + { + IMethodSymbol methodSymbol => ExtractFromMethod(methodSymbol, knownTypes, sendOnlyEndpointDefinition), + _ => SendOnlyEndpointSpecs.Empty + }; + } + + static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendOnlyEndpointGeneratorKnownTypes knownTypes, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) + { + var diagnostics = new List(); + var spec = ExtractSendOnlyEndpointSpec(methodSymbol, knownTypes, sendOnlyEndpointDefinition, diagnostics); + var sendOnlyEndpoints = spec is null + ? ImmutableEquatableArray.Empty + : ((SendOnlyEndpointSpec[])[spec]).ToImmutableEquatableArray(); + return new SendOnlyEndpointSpecs(sendOnlyEndpoints, diagnostics.ToImmutableEquatableArray()); + } + + static SendOnlyEndpointSpec? ExtractSendOnlyEndpointSpec( + IMethodSymbol method, + SendOnlyEndpointGeneratorKnownTypes knownTypes, + SendOnlyEndpointDefinition sendOnlyEndpointDefinition, + List diagnostics) + { + if (!TryGetSendOnlyEndpointAttribute(method, knownTypes.SendOnlyEndpointAttribute, out var sendOnlyEndpointAttribute) + || sendOnlyEndpointAttribute is null + || sendOnlyEndpointAttribute.ConstructorArguments.Length == 0 + || sendOnlyEndpointAttribute.ConstructorArguments[0].Value is not string endpointName) + { + return null; + } + + var problems = ImmutableList.CreateBuilder(); + + if (!method.IsStatic) + { + problems.Add("method must be static"); + } + + var expectedMethodName = $"Configure{endpointName}"; + if (!string.Equals(method.Name, expectedMethodName, StringComparison.OrdinalIgnoreCase)) + { + problems.Add($"method name must be '{expectedMethodName}'"); + } + + if (method.Parameters.Length == 0 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, knownTypes.EndpointConfiguration)) + { + problems.Add("first parameter must be EndpointConfiguration"); + } + + for (var i = 1; i < method.Parameters.Length; i++) + { + if (!IsAllowedConfigureMethodParameterType(method.Parameters[i].Type, knownTypes)) + { + problems.Add("parameters after EndpointConfiguration must be IServiceCollection, IConfiguration, or IHostEnvironment"); + break; + } + } + + if (problems.Count > 0) + { + diagnostics.Add(CreateDiagnostic(DiagnosticIds.InvalidSendOnlyEndpointMethodDescriptor, method, method.Name, string.Join(", ", problems))); + return null; + } + + var parameterTypeNames = new string[method.Parameters.Length]; + for (var i = 0; i < method.Parameters.Length; i++) + { + parameterTypeNames[i] = method.Parameters[i].Type.Name.ToLowerInvariant(); + } + + return new SendOnlyEndpointSpec( + endpointName, + sendOnlyEndpointDefinition.RegistrationMethodFullyQualified, + new ConfigureMethodSpec( + method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + method.Name, + parameterTypeNames.ToImmutableEquatableArray())); + } + + static bool TryGetSendOnlyEndpointAttribute(IMethodSymbol method, INamedTypeSymbol sendOnlyEndpointAttribute, out AttributeData? sendOnlyEndpointAttributeData) + { + foreach (var attribute in method.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, sendOnlyEndpointAttribute)) + { + sendOnlyEndpointAttributeData = attribute; + return true; + } + } + + sendOnlyEndpointAttributeData = null; + return false; + } + + static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, SendOnlyEndpointGeneratorKnownTypes knownTypes) + => SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IServiceCollection) + || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IConfiguration) + || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IHostEnvironment); + + static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] arguments) + { + var locations = symbol.Locations; + var location = locations.Length > 0 ? locations[0] : null; + return Diagnostic.Create(descriptor, location, arguments); + } + } + + internal readonly record struct ConfigureMethodSpec(string ContainingTypeFullyQualified, string MethodName, ImmutableEquatableArray ParameterTypeNames); + + internal sealed record SendOnlyEndpointSpec( + string EndpointName, + string RegistrationMethodFullyQualified, + ConfigureMethodSpec ConfigureMethod); + + internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray SendOnlyEndpoints, ImmutableEquatableArray Diagnostics) + { + public static SendOnlyEndpointSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); + } + + internal readonly record struct SendOnlyEndpointDefinition(string RegistrationMethodFullyQualified); + + readonly struct SendOnlyEndpointGeneratorKnownTypes( + INamedTypeSymbol sendOnlyEndpointAttribute, + INamedTypeSymbol endpointConfiguration, + INamedTypeSymbol iServiceCollection, + INamedTypeSymbol iConfiguration, + INamedTypeSymbol iHostEnvironment) + { + public INamedTypeSymbol SendOnlyEndpointAttribute { get; } = sendOnlyEndpointAttribute; + public INamedTypeSymbol EndpointConfiguration { get; } = endpointConfiguration; + public INamedTypeSymbol IServiceCollection { get; } = iServiceCollection; + public INamedTypeSymbol IConfiguration { get; } = iConfiguration; + public INamedTypeSymbol IHostEnvironment { get; } = iHostEnvironment; + + public static bool TryGet(Compilation compilation, out SendOnlyEndpointGeneratorKnownTypes knownTypes) + { + var sendOnlyEndpointAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute); + var endpointConfiguration = compilation.GetTypeByMetadataName(KnownTypeNames.EndpointConfigurationType); + var iServiceCollection = compilation.GetTypeByMetadataName(KnownTypeNames.IServiceCollection); + var iconfiguration = compilation.GetTypeByMetadataName(KnownTypeNames.IConfiguration); + var iHostEnvironment = compilation.GetTypeByMetadataName(KnownTypeNames.IHostEnvironment); + + if (sendOnlyEndpointAttribute is null + || endpointConfiguration is null + || iServiceCollection is null + || iconfiguration is null + || iHostEnvironment is null) + { + knownTypes = default; + return false; + } + + knownTypes = new SendOnlyEndpointGeneratorKnownTypes( + sendOnlyEndpointAttribute, + endpointConfiguration, + iServiceCollection, + iconfiguration, + iHostEnvironment); + + return true; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.TrackingNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.TrackingNames.cs new file mode 100644 index 00000000..246d8a92 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.TrackingNames.cs @@ -0,0 +1,15 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +public sealed partial class SendOnlyEndpointGenerator +{ + internal static class TrackingNames + { + public const string Extraction = nameof(Extraction); + public const string Diagnostics = nameof(Diagnostics); + public const string SendOnlyEndpoints = nameof(SendOnlyEndpoints); + public const string AssemblyClassName = nameof(AssemblyClassName); + public const string Combined = nameof(Combined); + + public static string[] All => [Extraction, Diagnostics, SendOnlyEndpoints, AssemblyClassName, Combined]; + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs new file mode 100644 index 00000000..8a2f202a --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -0,0 +1,58 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using Core.Analyzer; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +[Generator] +public sealed partial class SendOnlyEndpointGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + => InitializeGenerator(context, AzureServiceBusSendOnlyEndpoint); + + static void InitializeGenerator(IncrementalGeneratorInitializationContext context, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) + { + var extractionCandidates = context.SyntaxProvider + .ForAttributeWithMetadataName( + KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, + predicate: static (node, _) => node is MethodDeclarationSyntax, + transform: static (ctx, _) => ctx); + + var sendOnlyEndpointDefinitionProvider = context.CompilationProvider + .Select((_, _) => sendOnlyEndpointDefinition); + + var extractionResults = extractionCandidates + .Combine(sendOnlyEndpointDefinitionProvider) + .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) + .WithTrackingName(TrackingNames.Extraction); + + var diagnostics = extractionResults + .Collect() + .SelectMany(static (results, _) => + { + var diagnostics = new HashSet(); + foreach (var result in results) + { + diagnostics.UnionWith(result.Diagnostics); + } + return diagnostics.ToImmutableEquatableArray(); + }) + .WithTrackingName(TrackingNames.Diagnostics); + + context.RegisterSourceOutput(diagnostics, static (spc, diag) => spc.ReportDiagnostic(diag)); + + var sendOnlyEndpointSpecs = extractionResults + .SelectMany(static (result, _) => result.SendOnlyEndpoints) + .WithTrackingName(TrackingNames.SendOnlyEndpoints); + + var assemblyClassName = context.CompilationProvider + .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()) + .WithTrackingName(TrackingNames.AssemblyClassName); + + var combined = sendOnlyEndpointSpecs.Collect() + .Combine(assemblyClassName) + .WithTrackingName(TrackingNames.Combined); + + context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left, data.Right)); + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt index 2ace02f4..7791ee44 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt @@ -1,33 +1,4 @@ -// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.FunctionEndpointGenerator/FunctionMethodBodies.g.cs == -// - -#nullable enable annotations -#nullable disable warnings - -// Suppress warnings about [Obsolete] usage in generated code. -#pragma warning disable CS0612, CS0618 - -using Microsoft.Extensions.DependencyInjection; - -namespace Demo -{ - public partial class Functions - { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - public partial global::System.Threading.Tasks.Task Run( - global::Azure.Messaging.ServiceBus.ServiceBusReceivedMessage message, - global::Microsoft.Azure.Functions.Worker.ServiceBusMessageActions messageActions, - global::Microsoft.Azure.Functions.Worker.FunctionContext context, - global::System.Threading.CancellationToken cancellationToken) - { - var processor = context.InstanceServices - .GetRequiredKeyedService("ProcessOrder"); - return processor.Process(message, messageActions, context, cancellationToken); - } - } -} - -// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.FunctionEndpointGenerator/FunctionRegistration.g.cs == +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == // #nullable enable annotations @@ -39,7 +10,7 @@ namespace Demo namespace NServiceBus.Generated; /// -/// Registrations for NServiceBus functions in this assembly. +/// Registrations for NServiceBus send-only endpoints in this assembly. /// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] @@ -52,10 +23,6 @@ public static class GeneratedFunctionRegistrations_GeneratesSendOnlyEndpointRegi public static global::System.Collections.Generic.IEnumerable GetFunctionManifests() { - yield return new global::NServiceBus.FunctionManifest( - "ProcessOrder", "sales-queue", "AzureServiceBus", - (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.Functions.ConfigureProcessOrder(endpointconfiguration, iconfiguration, ihostenvironment), - global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } diff --git a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs index 0c799609..b975a35c 100644 --- a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs @@ -12,6 +12,7 @@ public class FunctionCompositionGeneratorTests public void GeneratesProjectComposition() => SourceGeneratorTest.ForIncrementalGenerator() .WithIncrementalGenerator() + .WithIncrementalGenerator() .WithSource(TestSources.ValidFunction) .ControlOutput(All) .WithProperty("build_property.OutputType", "Exe") diff --git a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs index 54d5d3ed..3c497f1b 100644 --- a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs @@ -38,13 +38,26 @@ public void GeneratesEndpointWithoutMessageActions() => [Test] public void GeneratesSendOnlyEndpointRegistration() => - SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.ValidFunction) + SourceGeneratorTest.ForIncrementalGenerator() .WithSource(""" namespace Demo; using Microsoft.Extensions.DependencyInjection; + file static class UsesGlobalTypes + { + public static void Use( + FunctionContext functionContext, + ServiceBusReceivedMessage message, + IConfiguration configuration, + IHostEnvironment environment) + { + CancellationToken cancellationToken = default; + _ = cancellationToken; + _ = Task.CompletedTask; + } + } + public static class ClientEndpoint { [NServiceBusSendOnlyEndpoint("client")] @@ -59,7 +72,7 @@ public static void ConfigureClient(EndpointConfiguration endpointConfiguration, [Test] public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() { - var result = SourceGeneratorTest.ForIncrementalGenerator() + var result = SourceGeneratorTest.ForIncrementalGenerator() .WithSource(""" namespace Demo; diff --git a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs index 6b82159a..f8f3c371 100644 --- a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs @@ -24,8 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Shape: FunctionEndpointGenerator.TriggerShape.RequiredAllowingAdditionalParameters( FunctionEndpointGenerator.ParameterRole.TriggerMessage, FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken)), - new FunctionEndpointGenerator.SendOnlyEndpointDefinition("global::Demo.Testing.TestSendOnlyEndpointManifestRegistration.Register")); + FunctionEndpointGenerator.ParameterRole.CancellationToken))); internal static class TrackingNames { diff --git a/src/Tests.Analyzers/MissingCompositionCallAnalyzerTests.cs b/src/Tests.Analyzers/MissingCompositionCallAnalyzerTests.cs index 03a3d988..5df55ace 100644 --- a/src/Tests.Analyzers/MissingCompositionCallAnalyzerTests.cs +++ b/src/Tests.Analyzers/MissingCompositionCallAnalyzerTests.cs @@ -80,6 +80,7 @@ public static void Configure(FunctionsApplicationBuilder builder) static SourceGeneratorTest CreateSourceGeneratorAnalyzerTest() => SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() .WithIncrementalGenerator() .WithAnalyzer() .BuildAs(OutputKind.ConsoleApplication) diff --git a/src/Tests.Analyzers/NoMessageActionsGenerator.cs b/src/Tests.Analyzers/NoMessageActionsGenerator.cs index 1515b7b4..6bebefff 100644 --- a/src/Tests.Analyzers/NoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/NoMessageActionsGenerator.cs @@ -24,8 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Shape: FunctionEndpointGenerator.TriggerShape.Required( FunctionEndpointGenerator.ParameterRole.TriggerMessage, FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken)), - new FunctionEndpointGenerator.SendOnlyEndpointDefinition("global::Demo.Testing.TestSendOnlyEndpointManifestRegistration.Register")); + FunctionEndpointGenerator.ParameterRole.CancellationToken))); internal static class TrackingNames { From bc00999424975caeec8069aa85eb546dd5011f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 10:43:13 +0200 Subject: [PATCH 04/25] Separate generator for send only --- .../CompilationAssemblyDetails.cs | 16 ++++- .../FunctionCompositionGenerator.Emitter.cs | 22 ++++--- .../FunctionCompositionGenerator.Parser.cs | 58 ++++++++++++++---- .../FunctionEndpointGenerator.Emitter.cs | 11 ---- ...FunctionEndpointGenerator.TrackingNames.cs | 2 + .../MissingCompositionCallAnalyzer.cs | 13 ++-- .../SendOnlyEndpointGenerator.Emitter.cs | 13 +--- .../SendOnlyEndpointGenerator.cs | 2 +- ...s.GeneratesProjectComposition.approved.txt | 55 ++++++++++++++++- ...apeAllowsAdditionalParameters.approved.txt | 11 +--- ...EndpointWithoutMessageActions.approved.txt | 11 +--- ...sts.GeneratesFunctionEndpoint.approved.txt | 11 +--- ...tionEndpointInGlobalNamespace.approved.txt | 11 +--- ...SendOnlyEndpointRegistration.approved.txt} | 13 +--- .../FunctionCompositionGeneratorTests.cs | 1 + .../FunctionEndpointGeneratorTests.cs | 60 ------------------- .../SendOnlyEndpointGeneratorTests.cs | 39 ++++++++++++ src/Tests.Analyzers/TestSources.cs | 28 +++++++++ 18 files changed, 215 insertions(+), 162 deletions(-) rename src/Tests.Analyzers/ApprovalFiles/{FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt => SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt} (79%) create mode 100644 src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs b/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs index c73782d3..755c6271 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/CompilationAssemblyDetails.cs @@ -7,10 +7,22 @@ readonly record struct CompilationAssemblyDetails(string SimpleName, string Iden { public static CompilationAssemblyDetails FromAssembly(IAssemblySymbol assembly) => new(assembly.Name, assembly.Identity.GetDisplayName()); - const string NamePrefix = "GeneratedFunctionRegistrations_"; + const string FunctionNamePrefix = "GeneratedFunctionRegistrations_"; + const string SendOnlyNamePrefix = "GeneratedSendOnlyEndpointRegistrations_"; + public string ToGenerationClassName() + { + return ToClassName(FunctionNamePrefix); + } + + public string ToSendOnlyGenerationClassName() + { + return ToClassName(SendOnlyNamePrefix); + } + + string ToClassName(string prefix) { var hash = NonCryptographicHash.GetHash(Identity); - return $"{NamePrefix}{SimpleName.Replace('.', '_')}_{hash:x16}"; + return $"{prefix}{SimpleName.Replace('.', '_')}_{hash:x16}"; } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs index c494c2ae..5f7bc00a 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Emitter.cs @@ -35,14 +35,20 @@ public static void Emit(SourceProductionContext context, CompositionSpec? compos foreach (var registrationClass in composition.RegistrationClasses) { context.CancellationToken.ThrowIfCancellationRequested(); - writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetFunctionManifests())"); - writer.WriteLine("{"); - writer.WriteLine(" manifest.Register(builder, manifest);"); - writer.WriteLine("}"); - writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetSendOnlyEndpointManifests())"); - writer.WriteLine("{"); - writer.WriteLine(" manifest.Register(builder, manifest);"); - writer.WriteLine("}"); + if (registrationClass.Kind == RegistrationClassKind.Function) + { + writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetFunctionManifests())"); + writer.WriteLine("{"); + writer.WriteLine(" manifest.Register(builder, manifest);"); + writer.WriteLine("}"); + } + else + { + writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetSendOnlyEndpointManifests())"); + writer.WriteLine("{"); + writer.WriteLine(" manifest.Register(builder, manifest);"); + writer.WriteLine("}"); + } } writer.CloseCurlies(); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs index 16052729..87a1919f 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.Parser.cs @@ -38,16 +38,27 @@ internal static HostProjectSpec ParseHostProject(AnalyzerConfigOptionsProvider p { cancellationToken.ThrowIfCancellationRequested(); - var registration = CreateGeneratedRegistrationClassSpec(referencedAssembly); - if (compilation.GetTypeByMetadataName(registration.FullClassName) is not null) - { - registrations.Add(registration); - } + AddGeneratedRegistrationClasses(compilation, referencedAssembly, registrations); } - if (hasLocalFunctions || hasLocalSendOnlyEndpoints) + if (hasLocalFunctions) { - registrations.Add(CreateGeneratedRegistrationClassSpec(compilation.Assembly)); + AddGeneratedRegistrationClass( + compilation, + $"NServiceBus.Generated.{CompilationAssemblyDetails.FromAssembly(compilation.Assembly).ToGenerationClassName()}", + RegistrationClassKind.Function, + registrations, + includeWithoutLookup: true); + } + + if (hasLocalSendOnlyEndpoints) + { + AddGeneratedRegistrationClass( + compilation, + $"NServiceBus.Generated.{CompilationAssemblyDetails.FromAssembly(compilation.Assembly).ToSendOnlyGenerationClassName()}", + RegistrationClassKind.SendOnly, + registrations, + includeWithoutLookup: true); } if (registrations.Count == 0) @@ -62,14 +73,39 @@ internal static HostProjectSpec ParseHostProject(AnalyzerConfigOptionsProvider p return new CompositionSpec(orderedRegistrations, hostProject.RootNamespace); } - static GeneratedRegistrationClassSpec CreateGeneratedRegistrationClassSpec(IAssemblySymbol assembly) + static void AddGeneratedRegistrationClasses( + Compilation compilation, + IAssemblySymbol assembly, + ISet registrations, + bool includeWithoutLookup = false) + { + var details = CompilationAssemblyDetails.FromAssembly(assembly); + + AddGeneratedRegistrationClass(compilation, $"NServiceBus.Generated.{details.ToGenerationClassName()}", RegistrationClassKind.Function, registrations, includeWithoutLookup); + AddGeneratedRegistrationClass(compilation, $"NServiceBus.Generated.{details.ToSendOnlyGenerationClassName()}", RegistrationClassKind.SendOnly, registrations, includeWithoutLookup); + } + + static void AddGeneratedRegistrationClass( + Compilation compilation, + string fullClassName, + RegistrationClassKind kind, + ISet registrations, + bool includeWithoutLookup) { - var className = CompilationAssemblyDetails.FromAssembly(assembly).ToGenerationClassName(); - return new GeneratedRegistrationClassSpec($"NServiceBus.Generated.{className}"); + if (includeWithoutLookup || compilation.GetTypeByMetadataName(fullClassName) is not null) + { + registrations.Add(new GeneratedRegistrationClassSpec(fullClassName, kind)); + } } } - internal readonly record struct GeneratedRegistrationClassSpec(string FullClassName); + internal readonly record struct GeneratedRegistrationClassSpec(string FullClassName, RegistrationClassKind Kind); + internal enum RegistrationClassKind + { + Function, + SendOnly + } + internal readonly record struct HostProjectSpec(bool IsHostProject, string? RootNamespace); internal sealed record CompositionSpec(ImmutableEquatableArray RegistrationClasses, string? RootNamespace); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs index e44fe4d6..e0a9912b 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs @@ -105,17 +105,6 @@ static void EmitRegistration(SourceProductionContext spc, ImmutableArray"); - writer.WriteLine("/// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly."); - writer.WriteLine("/// "); - writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); - writer.WriteLine(" GetSendOnlyEndpointManifests()"); - writer.WriteLine("{"); - writer.Indentation++; writer.WriteLine("yield break;"); writer.Indentation--; writer.WriteLine("}"); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs index 6a372534..a5ffe27e 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TrackingNames.cs @@ -9,5 +9,7 @@ internal static class TrackingNames public const string Functions = nameof(Functions); public const string AssemblyClassName = nameof(AssemblyClassName); public const string Combined = nameof(Combined); + + public static string[] All => [Extraction, Diagnostics, Functions, AssemblyClassName, Combined]; } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/MissingCompositionCallAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/MissingCompositionCallAnalyzer.cs index d6d8e970..225e4a62 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/MissingCompositionCallAnalyzer.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/MissingCompositionCallAnalyzer.cs @@ -82,8 +82,7 @@ static void OnCompilationStart(CompilationStartAnalysisContext context) static bool HasAutoGeneratedRegistrationTypes(Compilation compilation, CancellationToken cancellationToken) { - var currentClassName = CompilationAssemblyDetails.FromAssembly(compilation.Assembly).ToGenerationClassName(); - if (compilation.GetTypeByMetadataName($"NServiceBus.Generated.{currentClassName}") is not null) + if (HasRegistrationType(compilation, compilation.Assembly)) { return true; } @@ -91,14 +90,20 @@ static bool HasAutoGeneratedRegistrationTypes(Compilation compilation, Cancellat foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols) { cancellationToken.ThrowIfCancellationRequested(); - var className = CompilationAssemblyDetails.FromAssembly(assembly).ToGenerationClassName(); - if (compilation.GetTypeByMetadataName($"NServiceBus.Generated.{className}") is not null) + if (HasRegistrationType(compilation, assembly)) { return true; } } return false; + + static bool HasRegistrationType(Compilation compilation, IAssemblySymbol assembly) + { + var details = CompilationAssemblyDetails.FromAssembly(assembly); + return compilation.GetTypeByMetadataName($"NServiceBus.Generated.{details.ToGenerationClassName()}") is not null + || compilation.GetTypeByMetadataName($"NServiceBus.Generated.{details.ToSendOnlyGenerationClassName()}") is not null; + } } static string? InferCompositionNamespace(Compilation compilation, CancellationToken cancellationToken) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs index e631eede..228c8a8c 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs @@ -29,17 +29,6 @@ public static void Emit(SourceProductionContext spc, ImmutableArray"); - writer.WriteLine("/// Gets function manifests for NServiceBus functions in this assembly."); - writer.WriteLine("/// "); - writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); - writer.WriteLine(" GetFunctionManifests()"); - writer.WriteLine("{"); - writer.Indentation++; - writer.WriteLine("yield break;"); - writer.Indentation--; - writer.WriteLine("}"); - writer.WriteLine(); - writer.WriteLine("/// "); writer.WriteLine("/// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly."); writer.WriteLine("/// "); writer.WriteLine("public static global::System.Collections.Generic.IEnumerable"); @@ -70,4 +59,4 @@ static string GenerateConfigureMethodCall(ConfigureMethodSpec configureMethod) return $"(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => {configureMethod.ContainingTypeFullyQualified}.{configureMethod.MethodName}({argumentList})"; } } -} \ No newline at end of file +} diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs index 8a2f202a..0897c9e9 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -46,7 +46,7 @@ static void InitializeGenerator(IncrementalGeneratorInitializationContext contex .WithTrackingName(TrackingNames.SendOnlyEndpoints); var assemblyClassName = context.CompilationProvider - .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToGenerationClassName()) + .Select(static (c, _) => CompilationAssemblyDetails.FromAssembly(c.Assembly).ToSendOnlyGenerationClassName()) .WithTrackingName(TrackingNames.AssemblyClassName); var combined = sendOnlyEndpointSpecs.Collect() diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt index 980b998b..ac9ea85c 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt @@ -28,6 +28,33 @@ public partial class Functions } } +// == SendOnly.cs ====================================================================================================== +namespace Demo; + +using Microsoft.Extensions.DependencyInjection; + +file static class UsesGlobalTypes +{ + public static void Use( + FunctionContext functionContext, + ServiceBusReceivedMessage message, + IConfiguration configuration, + IHostEnvironment environment) + { + CancellationToken cancellationToken = default; + _ = cancellationToken; + _ = Task.CompletedTask; + } +} + +public static class ClientEndpoint +{ + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + } +} + // == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.FunctionCompositionGenerator/Composition.cs == // @@ -52,7 +79,7 @@ internal static class NServiceBusFunctionsComposition { manifest.Register(builder, manifest); } - foreach (var manifest in global::NServiceBus.Generated.GeneratedFunctionRegistrations_GeneratesProjectComposition_4d91953014478208.GetSendOnlyEndpointManifests()) + foreach (var manifest in global::NServiceBus.Generated.GeneratedSendOnlyEndpointRegistrations_GeneratesProjectComposition_4d91953014478208.GetSendOnlyEndpointManifests()) { manifest.Register(builder, manifest); } @@ -120,13 +147,37 @@ public static class GeneratedFunctionRegistrations_GeneratesProjectComposition_4 global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } +} + +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesProjectComposition_4d91953014478208 +{ /// /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. /// public static global::System.Collections.Generic.IEnumerable GetSendOnlyEndpointManifests() { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt index 70f06b30..0c741fff 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt @@ -63,13 +63,4 @@ public static class GeneratedFunctionRegistrations_GeneratesEndpointWithExtraUnr global::Demo.Testing.TestFunctionManifestRegistration.Register); yield break; } - - /// - /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. - /// - public static global::System.Collections.Generic.IEnumerable - GetSendOnlyEndpointManifests() - { - yield break; - } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt index ce7c4978..a9522ba4 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt @@ -62,13 +62,4 @@ public static class GeneratedFunctionRegistrations_GeneratesEndpointWithoutMessa global::Demo.Testing.TestFunctionManifestRegistration.Register); yield break; } - - /// - /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. - /// - public static global::System.Collections.Generic.IEnumerable - GetSendOnlyEndpointManifests() - { - yield break; - } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt index 4e3bb557..037937bc 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpoint.approved.txt @@ -58,13 +58,4 @@ public static class GeneratedFunctionRegistrations_GeneratesFunctionEndpoint_64a global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } - - /// - /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. - /// - public static global::System.Collections.Generic.IEnumerable - GetSendOnlyEndpointManifests() - { - yield break; - } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt index 74af0b02..83973260 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesFunctionEndpointInGlobalNamespace.approved.txt @@ -55,13 +55,4 @@ public static class GeneratedFunctionRegistrations_GeneratesFunctionEndpointInGl global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusFunction); yield break; } - - /// - /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. - /// - public static global::System.Collections.Generic.IEnumerable - GetSendOnlyEndpointManifests() - { - yield break; - } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt similarity index 79% rename from src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt rename to src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt index 7791ee44..778115f0 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt @@ -15,17 +15,8 @@ namespace NServiceBus.Generated; [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] [global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] -public static class GeneratedFunctionRegistrations_GeneratesSendOnlyEndpointRegistration_12baa95a3a042aa3 +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndpointRegistration_12baa95a3a042aa3 { - /// - /// Gets function manifests for NServiceBus functions in this assembly. - /// - public static global::System.Collections.Generic.IEnumerable - GetFunctionManifests() - { - yield break; - } - /// /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. /// @@ -38,4 +29,4 @@ public static class GeneratedFunctionRegistrations_GeneratesSendOnlyEndpointRegi global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs index b975a35c..0f05bc18 100644 --- a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs @@ -14,6 +14,7 @@ public void GeneratesProjectComposition() => .WithIncrementalGenerator() .WithIncrementalGenerator() .WithSource(TestSources.ValidFunction) + .WithSource(TestSources.ValidSendOnlyEndpoint, "SendOnly.cs") .ControlOutput(All) .WithProperty("build_property.OutputType", "Exe") .WithProperty("build_property.FunctionsExecutionModel", "isolated") diff --git a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs index 3c497f1b..e739a2af 100644 --- a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs @@ -36,61 +36,6 @@ public void GeneratesEndpointWithoutMessageActions() => .Run() .Approve(); - [Test] - public void GeneratesSendOnlyEndpointRegistration() => - SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(""" - namespace Demo; - - using Microsoft.Extensions.DependencyInjection; - - file static class UsesGlobalTypes - { - public static void Use( - FunctionContext functionContext, - ServiceBusReceivedMessage message, - IConfiguration configuration, - IHostEnvironment environment) - { - CancellationToken cancellationToken = default; - _ = cancellationToken; - _ = Task.CompletedTask; - } - } - - public static class ClientEndpoint - { - [NServiceBusSendOnlyEndpoint("client")] - public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) - { - } - } - """, "SendOnly.cs") - .Run() - .Approve(); - - [Test] - public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() - { - var result = SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(""" - namespace Demo; - - public class ClientEndpoint - { - [NServiceBusSendOnlyEndpoint("client")] - public void ConfigureClient(EndpointConfiguration endpointConfiguration) - { - } - } - """) - .SuppressCompilationErrors() - .SuppressDiagnosticErrors() - .Run(); - - Assert.That(result.GeneratorDiagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod)); - } - [Test] public void ReportsInvalidFunctionMethodWhenShapeContainsExtraUnrecognizedParameters() { @@ -628,11 +573,6 @@ public static class TestFunctionManifestRegistration public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.FunctionManifest __) { } } - public static class TestSendOnlyEndpointManifestRegistration - { - public static void Register(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder _, global::NServiceBus.SendOnlyEndpointManifest __) { } - } - {{classBody}} """; } diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs new file mode 100644 index 00000000..ab952d0c --- /dev/null +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -0,0 +1,39 @@ +namespace NServiceBus.AzureFunctions.Analyzers.Tests; + +using Microsoft.CodeAnalysis; +using NServiceBus.AzureFunctions.Analyzer; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class SendOnlyEndpointGeneratorTests +{ + [Test] + public void GeneratesSendOnlyEndpointRegistration() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidSendOnlyEndpoint, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() + { + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(""" + namespace Demo; + + public class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public void ConfigureClient(EndpointConfiguration endpointConfiguration) + { + } + } + """) + .SuppressCompilationErrors() + .SuppressDiagnosticErrors() + .Run(); + + Assert.That(result.GeneratorDiagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod)); + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index a975d55c..ea306ac9 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -99,4 +99,32 @@ public static void ConfigureProcessOrder( } } """; + + public const string ValidSendOnlyEndpoint = """ + namespace Demo; + + using Microsoft.Extensions.DependencyInjection; + + file static class UsesGlobalTypes + { + public static void Use( + FunctionContext functionContext, + ServiceBusReceivedMessage message, + IConfiguration configuration, + IHostEnvironment environment) + { + CancellationToken cancellationToken = default; + _ = cancellationToken; + _ = Task.CompletedTask; + } + } + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + } + } + """; } From 6fec1dde9bf8f40c24ed0f58efd3d7ac469dde80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 10:50:58 +0200 Subject: [PATCH 05/25] Fix formatting issues in approved test files and ensure proper closure of code blocks --- .../FunctionEndpointGenerator.Emitter.cs | 5 +---- ...onGeneratorTests.GeneratesProjectComposition.approved.txt | 2 +- ...arametersWhenShapeAllowsAdditionalParameters.approved.txt | 2 +- ...Tests.GeneratesEndpointWithoutMessageActions.approved.txt | 2 +- ...ointGeneratorTests.GeneratesFunctionEndpoint.approved.txt | 2 +- ...s.GeneratesFunctionEndpointInGlobalNamespace.approved.txt | 2 +- ...rTests.GeneratesSendOnlyEndpointRegistration.approved.txt | 2 +- 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs index e0a9912b..4e6c0d7a 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs @@ -106,10 +106,7 @@ static void EmitRegistration(SourceProductionContext spc, ImmutableArray Date: Wed, 3 Jun 2026 10:55:53 +0200 Subject: [PATCH 06/25] Fix function generator file formatting --- .../FunctionEndpointGenerator.Emitter.cs | 2 +- .../FunctionEndpointGenerator.Parser.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs index 4e6c0d7a..af9e880f 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Emitter.cs @@ -117,4 +117,4 @@ static string GenerateConfigureMethodCall(ConfigureMethodSpec configureMethod) return $"(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => {configureMethod.ContainingTypeFullyQualified}.{configureMethod.MethodName}({argumentList})"; } } -} \ No newline at end of file +} diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs index 25c7d7a0..91d8dfbb 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs @@ -675,4 +675,4 @@ public static bool TryGet(Compilation compilation, TriggerDefinition triggerDefi return true; } } -} \ No newline at end of file +} From 334a3b632f6c27096c7e1816bd2a8af0fd160290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 3 Jun 2026 11:10:37 +0200 Subject: [PATCH 07/25] Refactor usings in tests and remove global usings for clarity --- ...s.GeneratesProjectComposition.approved.txt | 34 +++------ ...apeAllowsAdditionalParameters.approved.txt | 5 -- ...EndpointWithoutMessageActions.approved.txt | 5 -- ...tionsForOrdinaryFunctionsOnly.approved.txt | 4 - .../ConfigurationAnalyzerTests.cs | 37 ++++++--- ...ctionEndpointGeneratorLenientShapeTests.cs | 5 ++ .../FunctionEndpointGeneratorTests.cs | 75 ++++++++++++++++++- .../SendOnlyEndpointGeneratorTests.cs | 4 +- src/Tests.Analyzers/SetUpFixture.cs | 18 +---- src/Tests.Analyzers/TestSources.cs | 41 ++++++---- 10 files changed, 148 insertions(+), 80 deletions(-) diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt index b4304e2a..8169c273 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt @@ -1,13 +1,12 @@ -// == GlobalUsings.cs ================================================================================================== -global using System.Threading; -global using System.Threading.Tasks; -global using Microsoft.Azure.Functions.Worker; -global using Azure.Messaging.ServiceBus; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.Hosting; -global using NServiceBus; - -// == Source01.cs ====================================================================================================== +// == Source00.cs ====================================================================================================== +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using NServiceBus; + namespace Demo; public partial class Functions @@ -29,24 +28,11 @@ public partial class Functions } // == SendOnly.cs ====================================================================================================== +using NServiceBus; namespace Demo; using Microsoft.Extensions.DependencyInjection; -file static class UsesGlobalTypes -{ - public static void Use( - FunctionContext functionContext, - ServiceBusReceivedMessage message, - IConfiguration configuration, - IHostEnvironment environment) - { - CancellationToken cancellationToken = default; - _ = cancellationToken; - _ = Task.CompletedTask; - } -} - public static class ClientEndpoint { [NServiceBusSendOnlyEndpoint("client")] diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt index 77d98b5d..d9d1e968 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorLenientShapeTests.GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAdditionalParameters.approved.txt @@ -1,8 +1,3 @@ -// == Compilation Diagnostics ========================================================================================== -GlobalUsings.cs(4,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(5,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(6,1): hidden CS8019: Unnecessary using directive. - // == NServiceBus.AzureFunctions.Analyzers.Tests/NServiceBus.AzureFunctions.Analyzers.Tests.LenientNoMessageActionsGenerator/FunctionMethodBodies.g.cs == // diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt index 2557dd15..3d076a05 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesEndpointWithoutMessageActions.approved.txt @@ -1,8 +1,3 @@ -// == Compilation Diagnostics ========================================================================================== -GlobalUsings.cs(4,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(5,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(6,1): hidden CS8019: Unnecessary using directive. - // == NServiceBus.AzureFunctions.Analyzers.Tests/NServiceBus.AzureFunctions.Analyzers.Tests.NoMessageActionsGenerator/FunctionMethodBodies.g.cs == // diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesNoRegistrationsForOrdinaryFunctionsOnly.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesNoRegistrationsForOrdinaryFunctionsOnly.approved.txt index 72695e4b..e69de29b 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesNoRegistrationsForOrdinaryFunctionsOnly.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionEndpointGeneratorTests.GeneratesNoRegistrationsForOrdinaryFunctionsOnly.approved.txt @@ -1,4 +0,0 @@ -// == Compilation Diagnostics ========================================================================================== -GlobalUsings.cs(5,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(6,1): hidden CS8019: Unnecessary using directive. -GlobalUsings.cs(7,1): hidden CS8019: Unnecessary using directive. \ No newline at end of file diff --git a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs index 0e6653f2..ac588f9a 100644 --- a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs +++ b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs @@ -8,6 +8,23 @@ namespace NServiceBus.AzureFunctions.Analyzers.Tests; [TestFixture] public class ConfigurationAnalyzerTests : AnalyzerTestFixture { + const string NServiceBusUsings = """ + using System.Threading.Tasks; + using NServiceBus; + + """; + + const string FunctionUsings = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using NServiceBus; + + """; + static readonly TestCaseData[] UnsupportedEndpointConfigurationCallCases = [ new("PurgeOnStartup(true)", DiagnosticIds.InvalidEndpointConfiguration), @@ -36,7 +53,7 @@ public class ConfigurationAnalyzerTests : AnalyzerTestFixture @@ -162,9 +186,15 @@ public void ReportsIHandleMessagesWarningOnlyOnceForMultipleAttributedMethods() { var result = SourceGeneratorTest.ForIncrementalGenerator() .WithSource(""" - namespace Demo; + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; - public partial class Functions : IHandleMessages + namespace Demo; + + public partial class Functions : IHandleMessages { [NServiceBusFunction] [Function("ProcessOrder")] @@ -213,6 +243,13 @@ public static void ConfigureProcessOrder2(EndpointConfiguration endpointConfigur } const string MultipleConfigureMethods = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.Configuration; + using NServiceBus; + namespace Demo; public partial class Functions @@ -238,6 +275,12 @@ public static void ConfigureProcessOrder( """; const string MissingAutoComplete = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo; public partial class Functions @@ -257,6 +300,12 @@ public static void ConfigureProcessOrder(EndpointConfiguration endpointConfigura """; const string AutoCompleteEnabled = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo; public partial class Functions @@ -283,6 +332,11 @@ public static void ConfigureProcessOrder(EndpointConfiguration endpointConfigura public void ReportsInvalidFunctionMethodWhenTriggerParameterMissing() { var diagnostic = GetInvalidFunctionMethodDiagnostic(""" + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo; public partial class Functions @@ -306,6 +360,12 @@ public static void ConfigureProcessOrder(EndpointConfiguration endpointConfigura public void ReportsInvalidFunctionMethodWhenMessageActionsRequiredButMissing() { var diagnostic = GetInvalidFunctionMethodDiagnostic(""" + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo; public partial class Functions @@ -471,6 +531,12 @@ public void ReportsInvalidFunctionMethodWhenServiceBusTriggerUsesTopicSubscripti { var result = SourceGeneratorTest.ForIncrementalGenerator() .WithSource(""" + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo; public partial class Functions @@ -558,6 +624,11 @@ static string NoMessageActionsSource(string classBody, bool triggerHasConstructo : ""; return $$""" + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo.Testing; [System.AttributeUsage(System.AttributeTargets.Parameter)] diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index ab952d0c..00179dee 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -20,6 +20,8 @@ public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() { var result = SourceGeneratorTest.ForIncrementalGenerator() .WithSource(""" + using NServiceBus; + namespace Demo; public class ClientEndpoint @@ -36,4 +38,4 @@ public void ConfigureClient(EndpointConfiguration endpointConfiguration) Assert.That(result.GeneratorDiagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod)); } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/SetUpFixture.cs b/src/Tests.Analyzers/SetUpFixture.cs index eb3cd58d..c3a70eac 100644 --- a/src/Tests.Analyzers/SetUpFixture.cs +++ b/src/Tests.Analyzers/SetUpFixture.cs @@ -16,16 +16,6 @@ namespace NServiceBus.AzureFunctions.Analyzers.Tests; [SetUpFixture] public class SetUpFixture { - const string CommonUsings = """ - global using System.Threading; - global using System.Threading.Tasks; - global using Microsoft.Azure.Functions.Worker; - global using Azure.Messaging.ServiceBus; - global using Microsoft.Extensions.Configuration; - global using Microsoft.Extensions.Hosting; - global using NServiceBus; - """; - static readonly ImmutableList ProjectReferences = [ MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), @@ -52,11 +42,9 @@ public class SetUpFixture public void OneTimeSetUp() { AnalyzerTest.ConfigureAllAnalyzerTests(test => test - .AddReferences(ProjectReferences) - .WithSource(CommonUsings, "GlobalUsings.cs")); + .AddReferences(ProjectReferences)); SourceGeneratorTest.ConfigureAllSourceGeneratorTests(test => test - .AddReferences(ProjectReferences) - .WithSource(CommonUsings, "GlobalUsings.cs")); + .AddReferences(ProjectReferences)); } -} \ No newline at end of file +} diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index ea306ac9..253e4c33 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -3,6 +3,11 @@ namespace NServiceBus.AzureFunctions.Analyzers.Tests; static class TestSources { public const string OrdinaryFunctionOnly = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + namespace Demo; public class Functions @@ -17,6 +22,14 @@ public Task Run( """; public const string ValidFunction = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using NServiceBus; + namespace Demo; public partial class Functions @@ -39,6 +52,11 @@ public static void ConfigureProcessOrder( """; public const string NoMessageActionsFunction = """ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using NServiceBus; + namespace Demo.Testing; [System.AttributeUsage(System.AttributeTargets.Parameter)] @@ -81,6 +99,14 @@ public static void ConfigureProcessOrder( """; public const string ValidFunctionInGlobalNamespace = """ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.ServiceBus; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using NServiceBus; + public partial class Functions { [NServiceBusFunction] @@ -101,24 +127,11 @@ public static void ConfigureProcessOrder( """; public const string ValidSendOnlyEndpoint = """ + using NServiceBus; namespace Demo; using Microsoft.Extensions.DependencyInjection; - file static class UsesGlobalTypes - { - public static void Use( - FunctionContext functionContext, - ServiceBusReceivedMessage message, - IConfiguration configuration, - IHostEnvironment environment) - { - CancellationToken cancellationToken = default; - _ = cancellationToken; - _ = Task.CompletedTask; - } - } - public static class ClientEndpoint { [NServiceBusSendOnlyEndpoint("client")] From b87c5eb4d6882faacdbe620cfe797580c346a744 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 11:50:50 +0200 Subject: [PATCH 08/25] Refactor attribute handling and diagnostics in endpoint generators --- .../ConfigurationAnalyzer.cs | 21 +-------- .../FunctionEndpointGenerator.Parser.cs | 41 +++++------------ .../SendOnlyEndpointGenerator.Parser.cs | 31 ++----------- .../SymbolExtensions.cs | 44 +++++++++++++++++++ .../TypeSymbolExtensions.cs | 17 +++++++ 5 files changed, 78 insertions(+), 76 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SymbolExtensions.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/TypeSymbolExtensions.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs index e6e89ef6..3aa10eb8 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs @@ -202,27 +202,10 @@ static bool HasSupportedConfigureMethodSignature(IMethodSymbol method, KnownSymb } static bool HasSendOnlyEndpointAttribute(IMethodSymbol method, INamedTypeSymbol? sendOnlyEndpointAttribute) - { - if (sendOnlyEndpointAttribute is null) - { - return false; - } - - foreach (var attribute in method.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, sendOnlyEndpointAttribute)) - { - return true; - } - } - - return false; - } + => sendOnlyEndpointAttribute is not null && method.HasAttribute(sendOnlyEndpointAttribute); static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, KnownSymbols knownSymbols) - => SymbolEqualityComparer.Default.Equals(parameterType, knownSymbols.IServiceCollection) - || SymbolEqualityComparer.Default.Equals(parameterType, knownSymbols.IConfiguration) - || SymbolEqualityComparer.Default.Equals(parameterType, knownSymbols.IHostEnvironment); + => parameterType.IsAllowedConfigureMethodParameterType(knownSymbols.IServiceCollection!, knownSymbols.IConfiguration!, knownSymbols.IHostEnvironment!); static string GetEndpointContextLabel(EndpointConfigurationContext endpointContext) => endpointContext == EndpointConfigurationContext.SendOnlyEndpoint ? SendOnlyEndpoints : AzureFunctionsEndpoints; diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs index 91d8dfbb..0a5a2f98 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs @@ -40,19 +40,19 @@ static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpo if (!methodSymbol.IsPartialDefinition) { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.MethodMustBePartialDescriptor, methodSymbol, methodSymbol.Name)); + diagnostics.Add(methodSymbol.CreateDiagnostic(DiagnosticIds.MethodMustBePartialDescriptor, methodSymbol.Name)); } var classSymbol = methodSymbol.ContainingType; if (!IsPartial(classSymbol, cancellationToken)) { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.ClassMustBePartialDescriptor, classSymbol, classSymbol.Name)); + diagnostics.Add(classSymbol.CreateDiagnostic(DiagnosticIds.ClassMustBePartialDescriptor, classSymbol.Name)); } if (ImplementsIHandleMessages(classSymbol, knownTypes)) { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.ShouldNotImplementIHandleMessagesDescriptor, classSymbol, classSymbol.Name)); + diagnostics.Add(classSymbol.CreateDiagnostic(DiagnosticIds.ShouldNotImplementIHandleMessagesDescriptor, classSymbol.Name)); } var spec = ExtractFunctionSpec(methodSymbol, knownTypes, triggerDefinition, diagnostics); @@ -224,12 +224,12 @@ static FunctionSpecs ExtractFromMethod(IMethodSymbol methodSymbol, FunctionEndpo if (autoCompleteMustBeDisabled) { - diagnostics.Add(CreateDiagnostic( - DiagnosticIds.AutoCompleteMustBeExplicitlyDisabled, - method, - method.Name, - autoCompletePropertyName!, - knownTypes.TriggerAttribute.Name)); + diagnostics.Add( + method.CreateDiagnostic( + DiagnosticIds.AutoCompleteMustBeExplicitlyDisabled, + method.Name, + autoCompletePropertyName!, + knownTypes.TriggerAttribute.Name)); } connectionSettingName ??= ""; @@ -473,7 +473,7 @@ static string FormatShape(ImmutableEquatableArray orderedParamete { if (configureMethod is not null) { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.MultipleConfigureMethodsDescriptor, functionClassType, configureMethodName, functionClassType.Name)); + diagnostics.Add(functionClassType.CreateDiagnostic(DiagnosticIds.MultipleConfigureMethodsDescriptor, configureMethodName, functionClassType.Name)); return null; } @@ -530,19 +530,7 @@ static bool ImplementsIHandleMessages(INamedTypeSymbol classSymbol, FunctionEndp } static bool TryGetFunctionAttribute(IMethodSymbol method, FunctionEndpointGeneratorKnownTypes knownTypes, [NotNullWhen(true)] out AttributeData? functionAttribute) - { - foreach (var attribute in method.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, knownTypes.FunctionAttribute)) - { - functionAttribute = attribute; - return true; - } - } - - functionAttribute = null!; - return false; - } + => method.TryGetAttribute(knownTypes.FunctionAttribute, out functionAttribute); static bool IsPartial(INamedTypeSymbol type, CancellationToken cancellationToken) { @@ -566,12 +554,7 @@ static bool IsPartial(INamedTypeSymbol type, CancellationToken cancellationToken return false; } - static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] arguments) - { - var locations = symbol.Locations; - var location = locations.Length > 0 ? locations[0] : null; - return Diagnostic.Create(descriptor, location, arguments); - } + } internal readonly record struct ConfigureMethodSpec(string ContainingTypeFullyQualified, string MethodName, ImmutableEquatableArray ParameterTypeNames); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs index ae084b2d..b00e1ce9 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -45,8 +45,7 @@ static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendO SendOnlyEndpointDefinition sendOnlyEndpointDefinition, List diagnostics) { - if (!TryGetSendOnlyEndpointAttribute(method, knownTypes.SendOnlyEndpointAttribute, out var sendOnlyEndpointAttribute) - || sendOnlyEndpointAttribute is null + if (!method.TryGetAttribute(knownTypes.SendOnlyEndpointAttribute, out var sendOnlyEndpointAttribute) || sendOnlyEndpointAttribute.ConstructorArguments.Length == 0 || sendOnlyEndpointAttribute.ConstructorArguments[0].Value is not string endpointName) { @@ -82,7 +81,7 @@ static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendO if (problems.Count > 0) { - diagnostics.Add(CreateDiagnostic(DiagnosticIds.InvalidSendOnlyEndpointMethodDescriptor, method, method.Name, string.Join(", ", problems))); + diagnostics.Add(method.CreateDiagnostic(DiagnosticIds.InvalidSendOnlyEndpointMethodDescriptor, method.Name, string.Join(", ", problems))); return null; } @@ -101,32 +100,8 @@ static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendO parameterTypeNames.ToImmutableEquatableArray())); } - static bool TryGetSendOnlyEndpointAttribute(IMethodSymbol method, INamedTypeSymbol sendOnlyEndpointAttribute, out AttributeData? sendOnlyEndpointAttributeData) - { - foreach (var attribute in method.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, sendOnlyEndpointAttribute)) - { - sendOnlyEndpointAttributeData = attribute; - return true; - } - } - - sendOnlyEndpointAttributeData = null; - return false; - } - static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, SendOnlyEndpointGeneratorKnownTypes knownTypes) - => SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IServiceCollection) - || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IConfiguration) - || SymbolEqualityComparer.Default.Equals(parameterType, knownTypes.IHostEnvironment); - - static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, ISymbol symbol, params object[] arguments) - { - var locations = symbol.Locations; - var location = locations.Length > 0 ? locations[0] : null; - return Diagnostic.Create(descriptor, location, arguments); - } + => parameterType.IsAllowedConfigureMethodParameterType(knownTypes.IServiceCollection, knownTypes.IConfiguration, knownTypes.IHostEnvironment); } internal readonly record struct ConfigureMethodSpec(string ContainingTypeFullyQualified, string MethodName, ImmutableEquatableArray ParameterTypeNames); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SymbolExtensions.cs b/src/NServiceBus.AzureFunctions.Analyzer/SymbolExtensions.cs new file mode 100644 index 00000000..f9919c32 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/SymbolExtensions.cs @@ -0,0 +1,44 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +static class SymbolExtensions +{ + extension(ISymbol symbol) + { + public bool HasAttribute(INamedTypeSymbol attributeType) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)) + { + return true; + } + } + + return false; + } + + public bool TryGetAttribute(INamedTypeSymbol attributeType, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (var attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)) + { + attributeData = attribute; + return true; + } + } + + attributeData = null; + return false; + } + + public Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, params object[] arguments) + { + var location = symbol.Locations.Length > 0 ? symbol.Locations[0] : null; + return Diagnostic.Create(descriptor, location, arguments); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/TypeSymbolExtensions.cs b/src/NServiceBus.AzureFunctions.Analyzer/TypeSymbolExtensions.cs new file mode 100644 index 00000000..87467ead --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/TypeSymbolExtensions.cs @@ -0,0 +1,17 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using Microsoft.CodeAnalysis; + +static class TypeSymbolExtensions +{ + extension(ITypeSymbol type) + { + public bool IsAllowedConfigureMethodParameterType( + INamedTypeSymbol iServiceCollection, + INamedTypeSymbol iConfiguration, + INamedTypeSymbol iHostEnvironment) + => SymbolEqualityComparer.Default.Equals(type, iServiceCollection) + || SymbolEqualityComparer.Default.Equals(type, iConfiguration) + || SymbolEqualityComparer.Default.Equals(type, iHostEnvironment); + } +} \ No newline at end of file From 5ee24c7e43eb3f7e68e4f3946672cf107143b958 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:07:47 +0200 Subject: [PATCH 09/25] Implement IDiagnosticsSpec interface and refactor diagnostic collection in endpoint generators --- .../DiagnosticsSpec.cs | 9 +++++++ .../DiagnosticsSpecExtensions.cs | 24 +++++++++++++++++++ .../FunctionEndpointGenerator.Parser.cs | 2 +- .../FunctionEndpointGenerator.cs | 15 +----------- .../SendOnlyEndpointGenerator.Parser.cs | 2 +- .../SendOnlyEndpointGenerator.cs | 11 +-------- 6 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpec.cs create mode 100644 src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpecExtensions.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpec.cs b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpec.cs new file mode 100644 index 00000000..83d76bea --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpec.cs @@ -0,0 +1,9 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using Core.Analyzer; +using Microsoft.CodeAnalysis; + +interface IDiagnosticsSpec +{ + ImmutableEquatableArray Diagnostics { get; } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpecExtensions.cs b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpecExtensions.cs new file mode 100644 index 00000000..568cf103 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticsSpecExtensions.cs @@ -0,0 +1,24 @@ +namespace NServiceBus.AzureFunctions.Analyzer; + +using System.Collections.Immutable; +using Core.Analyzer; +using Microsoft.CodeAnalysis; + +static class DiagnosticsSpecExtensions +{ + public static IEnumerable ToDiagnostics(this ImmutableArray results) where T : IDiagnosticsSpec + { + // DiagnosticWithInfo implements structural equality (Location, Info, AdditionalLocations) + // so HashSet deduplicates correctly. ImmutableEquatableArray enables incremental caching: + // unchanged documents reuse the same SyntaxTree references, so diagnostics compare equal + // across steps. Within an edited file, new tree references cause re-reporting, which is + // correct and cheap. + var diagnostics = new HashSet(); + foreach (var result in results) + { + diagnostics.UnionWith(result.Diagnostics); + } + + return diagnostics.ToImmutableEquatableArray(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs index 0a5a2f98..ede46367 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.Parser.cs @@ -575,7 +575,7 @@ internal sealed record FunctionSpec( string ProcessCallExpression, ConfigureMethodSpec ConfigureMethod); - internal readonly record struct FunctionSpecs(ImmutableEquatableArray Functions, ImmutableEquatableArray Diagnostics) + internal readonly record struct FunctionSpecs(ImmutableEquatableArray Functions, ImmutableEquatableArray Diagnostics) : IDiagnosticsSpec { public static FunctionSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index dddad590..3bc42b2d 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -27,20 +27,7 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte var diagnostics = extractionResults .Collect() - .SelectMany(static (results, _) => - { - // DiagnosticWithInfo implements structural equality (Location, Info, AdditionalLocations) - // so HashSet deduplicates correctly. ImmutableEquatableArray enables incremental caching: - // unchanged documents reuse the same SyntaxTree references, so diagnostics compare equal - // across steps. Within an edited file, new tree references cause re-reporting, which is - // correct and cheap. - var diagnostics = new HashSet(); - foreach (var result in results) - { - diagnostics.UnionWith(result.Diagnostics); - } - return diagnostics.ToImmutableEquatableArray(); - }) + .SelectMany(static (results, _) => results.ToDiagnostics()) .WithTrackingName(TrackingNames.Diagnostics); context.RegisterSourceOutput(diagnostics, static (spc, diag) => diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs index b00e1ce9..de4bb54d 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -111,7 +111,7 @@ internal sealed record SendOnlyEndpointSpec( string RegistrationMethodFullyQualified, ConfigureMethodSpec ConfigureMethod); - internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray SendOnlyEndpoints, ImmutableEquatableArray Diagnostics) + internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray SendOnlyEndpoints, ImmutableEquatableArray Diagnostics) : IDiagnosticsSpec { public static SendOnlyEndpointSpecs Empty { get; } = new(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); } diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs index 0897c9e9..fc37e93d 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -1,6 +1,5 @@ namespace NServiceBus.AzureFunctions.Analyzer; -using Core.Analyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -28,15 +27,7 @@ static void InitializeGenerator(IncrementalGeneratorInitializationContext contex var diagnostics = extractionResults .Collect() - .SelectMany(static (results, _) => - { - var diagnostics = new HashSet(); - foreach (var result in results) - { - diagnostics.UnionWith(result.Diagnostics); - } - return diagnostics.ToImmutableEquatableArray(); - }) + .SelectMany(static (results, _) => results.ToDiagnostics()) .WithTrackingName(TrackingNames.Diagnostics); context.RegisterSourceOutput(diagnostics, static (spc, diag) => spc.ReportDiagnostic(diag)); From e33aa99eba52fc2e61ce181cac3b5dfcd536619b Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:10:09 +0200 Subject: [PATCH 10/25] Add tests for SendOnlyEndpointGenerator and validate endpoint generation scenarios --- ...atesMultipleSendOnlyEndpoints.approved.txt | 36 ++++ ...ationsWhenNoSendOnlyEndpoints.approved.txt | 2 + ...OnlyEndpointInGlobalNamespace.approved.txt | 32 ++++ ...ntWithAllAdditionalParameters.approved.txt | 32 ++++ ...intWithNoAdditionalParameters.approved.txt | 32 ++++ .../SendOnlyEndpointGeneratorTests.cs | 157 ++++++++++++++++++ src/Tests.Analyzers/TestSources.cs | 89 +++++++++- 7 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt new file mode 100644 index 00000000..afc73eeb --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt @@ -0,0 +1,36 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesMultipleSendOnlyEndpoints_0a5bc3aa83395047 +{ + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "sender", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.SenderEndpoint.ConfigureSender(endpointconfiguration, iconfiguration, ihostenvironment), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt new file mode 100644 index 00000000..b508e4c4 --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt @@ -0,0 +1,2 @@ +// == Compilation Diagnostics ========================================================================================== +SendOnly.cs(1,1): hidden CS8019: Unnecessary using directive. \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt new file mode 100644 index 00000000..ae5d81dd --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt @@ -0,0 +1,32 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndpointInGlobalNamespace_e2e010c95a40b63d +{ + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt new file mode 100644 index 00000000..a37546d9 --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt @@ -0,0 +1,32 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndpointWithAllAdditionalParameters_a2de95ba8b4b7d2e +{ + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt new file mode 100644 index 00000000..9a6f4bdd --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt @@ -0,0 +1,32 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndpointWithNoAdditionalParameters_5427349d0ebb643e +{ + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index 00179dee..4ad4aab1 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -15,6 +15,41 @@ public void GeneratesSendOnlyEndpointRegistration() => .Run() .Approve(); + [Test] + public void GeneratesSendOnlyEndpointInGlobalNamespace() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidSendOnlyEndpointInGlobalNamespace, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void GeneratesSendOnlyEndpointWithAllAdditionalParameters() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidSendOnlyEndpointWithAllAdditionalParameters, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void GeneratesSendOnlyEndpointWithNoAdditionalParameters() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidSendOnlyEndpointWithNoAdditionalParameters, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void GeneratesNoRegistrationsWhenNoSendOnlyEndpoints() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.NoSendOnlyEndpoints, "SendOnly.cs") + .Run() + .Approve(); + + [Test] + public void GeneratesMultipleSendOnlyEndpoints() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.MultipleSendOnlyEndpoints, "SendOnly.cs") + .Run() + .Approve(); + [Test] public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() { @@ -38,4 +73,126 @@ public void ConfigureClient(EndpointConfiguration endpointConfiguration) Assert.That(result.GeneratorDiagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod)); } + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenMethodNameWrong() + { + var diagnostic = GetInvalidSendOnlyEndpointMethodDiagnostic(""" + using NServiceBus; + + namespace Demo; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void WrongName(EndpointConfiguration endpointConfiguration) + { + } + } + """); + + Assert.That(diagnostic.GetMessage(), Does.Contain("method name must be 'Configureclient'")); + } + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenFirstParameterNotEndpointConfiguration() + { + var diagnostic = GetInvalidSendOnlyEndpointMethodDiagnostic(""" + using NServiceBus; + + namespace Demo; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(string wrongParam) + { + } + } + """); + + Assert.That(diagnostic.GetMessage(), Does.Contain("first parameter must be EndpointConfiguration")); + } + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenNoParameters() + { + var diagnostic = GetInvalidSendOnlyEndpointMethodDiagnostic(""" + using NServiceBus; + + namespace Demo; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient() + { + } + } + """); + + Assert.That(diagnostic.GetMessage(), Does.Contain("first parameter must be EndpointConfiguration")); + } + + [Test] + public void ReportsInvalidSendOnlyEndpointMethodWhenInvalidAdditionalParameter() + { + var diagnostic = GetInvalidSendOnlyEndpointMethodDiagnostic(""" + using NServiceBus; + + namespace Demo; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, string invalidParam) + { + } + } + """); + + Assert.That(diagnostic.GetMessage(), Does.Contain("parameters after EndpointConfiguration must be IServiceCollection, IConfiguration, or IHostEnvironment")); + } + + [Test] + public void ReportsAllProblemsInSingleDiagnostic() + { + var diagnostic = GetInvalidSendOnlyEndpointMethodDiagnostic(""" + using NServiceBus; + + namespace Demo; + + public class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public void WrongName(string invalidParam) + { + } + } + """); + + var message = diagnostic.GetMessage(); + Assert.That(message, Does.Contain("method must be static")); + Assert.That(message, Does.Contain("method name must be 'Configureclient'")); + Assert.That(message, Does.Contain("first parameter must be EndpointConfiguration")); + } + + #region Helpers + + static Diagnostic GetInvalidSendOnlyEndpointMethodDiagnostic(string source) + { + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source) + .SuppressCompilationErrors() + .SuppressDiagnosticErrors() + .Run(); + + var diagnostics = result.GeneratorDiagnostics; + Assert.That(diagnostics, Has.Some.Matches(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod), + "Expected InvalidSendOnlyEndpointMethod diagnostic to be reported"); + + return diagnostics.First(d => d.Id == DiagnosticIds.InvalidSendOnlyEndpointMethod); + } + + #endregion } diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index 253e4c33..5002e05d 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -140,4 +140,91 @@ public static void ConfigureClient(EndpointConfiguration endpointConfiguration, } } """; -} + + public const string ValidSendOnlyEndpointInGlobalNamespace = """ + using NServiceBus; + using Microsoft.Extensions.DependencyInjection; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + } + } + """; + + public const string ValidSendOnlyEndpointWithAllAdditionalParameters = """ + using NServiceBus; + namespace Demo; + + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient( + EndpointConfiguration endpointConfiguration, + IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment) + { + } + } + """; + + public const string ValidSendOnlyEndpointWithNoAdditionalParameters = """ + using NServiceBus; + namespace Demo; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration) + { + } + } + """; + + public const string MultipleSendOnlyEndpoints = """ + using NServiceBus; + namespace Demo; + + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client")] + public static void ConfigureClient( + EndpointConfiguration endpointConfiguration, + IServiceCollection services) + { + } + } + + public static class SenderEndpoint + { + [NServiceBusSendOnlyEndpoint("sender")] + public static void ConfigureSender( + EndpointConfiguration endpointConfiguration, + IConfiguration configuration, + IHostEnvironment environment) + { + } + } + """; + + public const string NoSendOnlyEndpoints = """ + using NServiceBus; + namespace Demo; + + public static class SomeClass + { + public static void DoSomething() { } + } + """; +} \ No newline at end of file From 2393be05298eb6bfa3a349a3a323d6275fcb88e7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:27:12 +0200 Subject: [PATCH 11/25] Add AssertRunsAreEqual to all generator approval tests --- .../FunctionCompositionGeneratorTests.cs | 3 ++- ...nctionEndpointGeneratorLenientShapeTests.cs | 3 ++- .../FunctionEndpointGeneratorTests.cs | 12 ++++++++---- .../SendOnlyEndpointGeneratorTests.cs | 18 ++++++++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs index 0f05bc18..b39bf139 100644 --- a/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionCompositionGeneratorTests.cs @@ -21,5 +21,6 @@ public void GeneratesProjectComposition() => .WithProperty("build_property.RootNamespace", "My.FunctionApp") .SuppressCompilationErrors() .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); } \ No newline at end of file diff --git a/src/Tests.Analyzers/FunctionEndpointGeneratorLenientShapeTests.cs b/src/Tests.Analyzers/FunctionEndpointGeneratorLenientShapeTests.cs index 89483606..0a6c34d9 100644 --- a/src/Tests.Analyzers/FunctionEndpointGeneratorLenientShapeTests.cs +++ b/src/Tests.Analyzers/FunctionEndpointGeneratorLenientShapeTests.cs @@ -11,7 +11,8 @@ public void GeneratesEndpointWithExtraUnrecognizedParametersWhenShapeAllowsAddit SourceGeneratorTest.ForIncrementalGenerator() .WithSource(SourceWithAdditionalParameter) .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); const string SourceWithAdditionalParameter = """ using System.Threading; diff --git a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs index b50ec8aa..b12d1f91 100644 --- a/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/FunctionEndpointGeneratorTests.cs @@ -13,28 +13,32 @@ public void GeneratesFunctionEndpoint() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidFunction) .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesFunctionEndpointInGlobalNamespace() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidFunctionInGlobalNamespace) .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesNoRegistrationsForOrdinaryFunctionsOnly() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.OrdinaryFunctionOnly) .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesEndpointWithoutMessageActions() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.NoMessageActionsFunction) .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void ReportsInvalidFunctionMethodWhenShapeContainsExtraUnrecognizedParameters() diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index 4ad4aab1..c190f66a 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -13,42 +13,48 @@ public void GeneratesSendOnlyEndpointRegistration() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidSendOnlyEndpoint, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesSendOnlyEndpointInGlobalNamespace() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidSendOnlyEndpointInGlobalNamespace, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesSendOnlyEndpointWithAllAdditionalParameters() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidSendOnlyEndpointWithAllAdditionalParameters, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesSendOnlyEndpointWithNoAdditionalParameters() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.ValidSendOnlyEndpointWithNoAdditionalParameters, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesNoRegistrationsWhenNoSendOnlyEndpoints() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.NoSendOnlyEndpoints, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void GeneratesMultipleSendOnlyEndpoints() => SourceGeneratorTest.ForIncrementalGenerator() .WithSource(TestSources.MultipleSendOnlyEndpoints, "SendOnly.cs") .Run() - .Approve(); + .Approve() + .AssertRunsAreEqual(); [Test] public void ReportsInvalidSendOnlyEndpointMethodWhenMethodIsNotStatic() From a4e76b36d571f56e4ae14de77ce896c5a97e67bf Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:41:56 +0200 Subject: [PATCH 12/25] Remove unused using directive from FunctionEndpointGenerator --- .../FunctionEndpointGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 3bc42b2d..5f144097 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -1,6 +1,5 @@ namespace NServiceBus.AzureFunctions.Analyzer; -using Core.Analyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; From 0c7f3206dc5096b283aef670b6e3d2d436e38b45 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:47:31 +0200 Subject: [PATCH 13/25] Inline SendOnlyEndpointDefinition into static Select lambda --- .../FunctionEndpointGenerator.cs | 8 ++------ .../SendOnlyEndpointGenerator.AzureServiceBus.cs | 7 ------- .../SendOnlyEndpointGenerator.cs | 9 ++------- 3 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 5f144097..1cdc2c50 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -17,7 +17,8 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); - var triggerDefinitionProvider = CreateTriggerDefinitionProvider(context, triggerDefinition); + var triggerDefinitionProvider = context.CompilationProvider + .Select((_, _) => triggerDefinition); var extractionResults = extractionCandidates .Combine(triggerDefinitionProvider) @@ -45,10 +46,5 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte .WithTrackingName(TrackingNames.Combined); context.RegisterSourceOutput(combined, static (spc, data) => Emitter.Emit(spc, data.Left, data.Right)); - - static IncrementalValueProvider CreateTriggerDefinitionProvider( - IncrementalGeneratorInitializationContext context, - TriggerDefinition triggerDefinition) => - context.CompilationProvider.Select((_, _) => triggerDefinition); } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs deleted file mode 100644 index 10b74828..00000000 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.AzureServiceBus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NServiceBus.AzureFunctions.Analyzer; - -public sealed partial class SendOnlyEndpointGenerator -{ - static readonly SendOnlyEndpointDefinition AzureServiceBusSendOnlyEndpoint = new( - RegistrationMethodFullyQualified: $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"); -} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs index fc37e93d..e5275382 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -7,9 +7,6 @@ namespace NServiceBus.AzureFunctions.Analyzer; public sealed partial class SendOnlyEndpointGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) - => InitializeGenerator(context, AzureServiceBusSendOnlyEndpoint); - - static void InitializeGenerator(IncrementalGeneratorInitializationContext context, SendOnlyEndpointDefinition sendOnlyEndpointDefinition) { var extractionCandidates = context.SyntaxProvider .ForAttributeWithMetadataName( @@ -17,11 +14,9 @@ static void InitializeGenerator(IncrementalGeneratorInitializationContext contex predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); - var sendOnlyEndpointDefinitionProvider = context.CompilationProvider - .Select((_, _) => sendOnlyEndpointDefinition); - var extractionResults = extractionCandidates - .Combine(sendOnlyEndpointDefinitionProvider) + .Combine(context.CompilationProvider.Select(static (_, _) => new SendOnlyEndpointDefinition( + $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"))) .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) .WithTrackingName(TrackingNames.Extraction); From 129707cb6bb4eb7c75ded1a616610f09413df2f4 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 12:57:17 +0200 Subject: [PATCH 14/25] Hardcode registration method name in SendOnlyEndpointDefinition --- .../SendOnlyEndpointGenerator.Parser.cs | 7 ++++++- .../SendOnlyEndpointGenerator.cs | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs index de4bb54d..d1839a1e 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -116,7 +116,12 @@ internal readonly record struct SendOnlyEndpointSpecs(ImmutableEquatableArray.Empty, ImmutableEquatableArray.Empty); } - internal readonly record struct SendOnlyEndpointDefinition(string RegistrationMethodFullyQualified); + internal readonly record struct SendOnlyEndpointDefinition + { + public string RegistrationMethodFullyQualified { get; } = $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"; + + public SendOnlyEndpointDefinition() { } + } readonly struct SendOnlyEndpointGeneratorKnownTypes( INamedTypeSymbol sendOnlyEndpointAttribute, diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs index e5275382..3869c1d1 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -15,8 +15,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: static (ctx, _) => ctx); var extractionResults = extractionCandidates - .Combine(context.CompilationProvider.Select(static (_, _) => new SendOnlyEndpointDefinition( - $"global::{KnownTypeNames.AzureServiceBusFunctionsHostApplicationBuilderExtensions}.{KnownTypeNames.AddNServiceBusAzureServiceBusSendOnlyEndpoint}"))) + .Combine(context.CompilationProvider.Select(static (_, _) => new SendOnlyEndpointDefinition())) .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) .WithTrackingName(TrackingNames.Extraction); From d68f79cd3dc2e50b3c3e3ec081f9a892e9a97c10 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 13:26:45 +0200 Subject: [PATCH 15/25] Remove file specification and unnecessary using --- ...FunctionEndpointGenerator.AzureServiceBus.cs | 17 +++++++---------- ...trationsWhenNoSendOnlyEndpoints.approved.txt | 2 -- .../SendOnlyEndpointGeneratorTests.cs | 12 ++++++------ src/Tests.Analyzers/TestSources.cs | 1 - 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs index a5ea280c..f83e8fad 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.AzureServiceBus.cs @@ -4,14 +4,8 @@ namespace NServiceBus.AzureFunctions.Analyzer; public sealed partial class FunctionEndpointGenerator { - static readonly ParameterRole MessageActions = new("MessageActions"); - - static readonly TriggerDefinition AzureServiceBusTrigger = new( - TriggerAttributeMetadataName: "Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute", - AdditionalParameterTypes: new AdditionalParameterType[] - { - new("Microsoft.Azure.Functions.Worker.ServiceBusMessageActions", MessageActions) - }.ToImmutableEquatableArray(), + internal sealed record AzureServiceBusTriggerDefinition() : TriggerDefinition(TriggerAttributeMetadataName: "Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute", + AdditionalParameterTypes: new AdditionalParameterType[] { new("Microsoft.Azure.Functions.Worker.ServiceBusMessageActions", MessageActions) }.ToImmutableEquatableArray(), ProcessorTypeFullyQualified: "global::NServiceBus.AzureFunctions.AzureServiceBus.AzureServiceBusMessageProcessor", AddressExtraction: AddressExtractionPolicy.FromNamedConstructorParameter("queueName"), ConnectionSetting: ConnectionSettingPolicy.FromNamedProperty("Connection"), @@ -22,5 +16,8 @@ public sealed partial class FunctionEndpointGenerator ParameterRole.TriggerMessage, MessageActions, ParameterRole.FunctionContext, - ParameterRole.CancellationToken)); -} + ParameterRole.CancellationToken)) + { + static readonly ParameterRole MessageActions = new("MessageActions"); + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt index b508e4c4..e69de29b 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesNoRegistrationsWhenNoSendOnlyEndpoints.approved.txt @@ -1,2 +0,0 @@ -// == Compilation Diagnostics ========================================================================================== -SendOnly.cs(1,1): hidden CS8019: Unnecessary using directive. \ No newline at end of file diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index c190f66a..8c726ac5 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -11,7 +11,7 @@ public class SendOnlyEndpointGeneratorTests [Test] public void GeneratesSendOnlyEndpointRegistration() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.ValidSendOnlyEndpoint, "SendOnly.cs") + .WithSource(TestSources.ValidSendOnlyEndpoint) .Run() .Approve() .AssertRunsAreEqual(); @@ -19,7 +19,7 @@ public void GeneratesSendOnlyEndpointRegistration() => [Test] public void GeneratesSendOnlyEndpointInGlobalNamespace() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.ValidSendOnlyEndpointInGlobalNamespace, "SendOnly.cs") + .WithSource(TestSources.ValidSendOnlyEndpointInGlobalNamespace) .Run() .Approve() .AssertRunsAreEqual(); @@ -27,7 +27,7 @@ public void GeneratesSendOnlyEndpointInGlobalNamespace() => [Test] public void GeneratesSendOnlyEndpointWithAllAdditionalParameters() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.ValidSendOnlyEndpointWithAllAdditionalParameters, "SendOnly.cs") + .WithSource(TestSources.ValidSendOnlyEndpointWithAllAdditionalParameters) .Run() .Approve() .AssertRunsAreEqual(); @@ -35,7 +35,7 @@ public void GeneratesSendOnlyEndpointWithAllAdditionalParameters() => [Test] public void GeneratesSendOnlyEndpointWithNoAdditionalParameters() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.ValidSendOnlyEndpointWithNoAdditionalParameters, "SendOnly.cs") + .WithSource(TestSources.ValidSendOnlyEndpointWithNoAdditionalParameters) .Run() .Approve() .AssertRunsAreEqual(); @@ -43,7 +43,7 @@ public void GeneratesSendOnlyEndpointWithNoAdditionalParameters() => [Test] public void GeneratesNoRegistrationsWhenNoSendOnlyEndpoints() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.NoSendOnlyEndpoints, "SendOnly.cs") + .WithSource(TestSources.NoSendOnlyEndpoints) .Run() .Approve() .AssertRunsAreEqual(); @@ -51,7 +51,7 @@ public void GeneratesNoRegistrationsWhenNoSendOnlyEndpoints() => [Test] public void GeneratesMultipleSendOnlyEndpoints() => SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(TestSources.MultipleSendOnlyEndpoints, "SendOnly.cs") + .WithSource(TestSources.MultipleSendOnlyEndpoints) .Run() .Approve() .AssertRunsAreEqual(); diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index 5002e05d..716b8344 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -219,7 +219,6 @@ public static void ConfigureSender( """; public const string NoSendOnlyEndpoints = """ - using NServiceBus; namespace Demo; public static class SomeClass From 32ed16db1fe73aafc26cd91178d1d73f14edf432 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 13:31:49 +0200 Subject: [PATCH 16/25] Remove the send-only API name --- ...reServiceBusFunctionsHostApplicationBuilderExtensions.cs | 2 +- .../FunctionEndpointConfigurationBuilder.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs index c3b000d4..abcea749 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs @@ -21,7 +21,7 @@ public static void AddNServiceBusAzureServiceBusFunction(this FunctionsApplicati { builder.Services.AddAzureClientsCore(); - var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest, $"[{nameof(NServiceBusSendOnlyEndpointAttribute)}]"); + var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest); var transport = GetAzureServiceBusTransport(endpointConfiguration); var resolvedConnectionSettingName = string.IsNullOrWhiteSpace(functionManifest.ConnectionSettingName) diff --git a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs index 70c07db2..4135eff9 100644 --- a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs +++ b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs @@ -19,12 +19,10 @@ public static class FunctionEndpointConfigurationBuilder /// /// The Functions application builder the endpoint is attached to. /// The manifest describing the endpoint to configure. - /// Name of the send-only API included in the exception thrown when a send-only configuration is detected. /// Thrown when the supplied manifest configures the endpoint as send-only. public static EndpointConfiguration BuildReceiveEndpointConfiguration( FunctionsApplicationBuilder builder, - FunctionManifest functionManifest, - string sendOnlyEndpointApiName) + FunctionManifest functionManifest) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(functionManifest); @@ -37,7 +35,7 @@ public static EndpointConfiguration BuildReceiveEndpointConfiguration( if (endpointConfiguration.IsSendOnly) { - throw new InvalidOperationException($"Functions can't be send-only endpoints, use {sendOnlyEndpointApiName}."); + throw new InvalidOperationException($"Functions can't be send-only endpoints, use [{typeof(NServiceBusSendOnlyEndpointAttribute)}] to create send-only endpoints."); } var resolvedAddress = FunctionBindingExpression.Resolve(functionManifest.Address, builder.Configuration); From c340d96e61144e71573ceeeb29a6ec54f0045008 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 13:54:28 +0200 Subject: [PATCH 17/25] Inline trigger definition to have a pure incremental pipeline --- src/IntegrationTestApp/ClientEndpoint.cs | 2 +- ...tionEndpointGenerator.TriggerDefinition.cs | 2 +- .../FunctionEndpointGenerator.cs | 11 +++---- .../LenientNoMessageActionsGenerator.cs | 29 ++++++++--------- .../NoMessageActionsGenerator.cs | 31 ++++++++++--------- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/IntegrationTestApp/ClientEndpoint.cs b/src/IntegrationTestApp/ClientEndpoint.cs index 47b4b976..ff1c6af9 100644 --- a/src/IntegrationTestApp/ClientEndpoint.cs +++ b/src/IntegrationTestApp/ClientEndpoint.cs @@ -17,4 +17,4 @@ public static void ConfigureClient(EndpointConfiguration endpointConfiguration, routing.RouteToEndpoint(typeof(SubmitOrder), "sales"); endpointConfiguration.UseSerialization(); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TriggerDefinition.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TriggerDefinition.cs index 1705fcba..be7061cf 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TriggerDefinition.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.TriggerDefinition.cs @@ -22,7 +22,7 @@ public sealed partial class FunctionEndpointGenerator /// Trigger method signatures are validated against . /// /// - internal sealed record TriggerDefinition( + internal abstract record TriggerDefinition( string TriggerAttributeMetadataName, ImmutableEquatableArray AdditionalParameterTypes, string ProcessorTypeFullyQualified, diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index 1cdc2c50..f808b61d 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -6,10 +6,10 @@ namespace NServiceBus.AzureFunctions.Analyzer; [Generator] public sealed partial class FunctionEndpointGenerator : IIncrementalGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) - => InitializeGenerator(context, AzureServiceBusTrigger); + public void Initialize(IncrementalGeneratorInitializationContext context) => InitializeGenerator(context); - internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context, TriggerDefinition triggerDefinition) + internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context) + where TDefinition : TriggerDefinition, new() { var extractionCandidates = context.SyntaxProvider .ForAttributeWithMetadataName( @@ -17,11 +17,8 @@ internal static void InitializeGenerator(IncrementalGeneratorInitializationConte predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); - var triggerDefinitionProvider = context.CompilationProvider - .Select((_, _) => triggerDefinition); - var extractionResults = extractionCandidates - .Combine(triggerDefinitionProvider) + .Combine(context.CompilationProvider.Select(static (_, _) => new TDefinition())) .Select(static (pair, ct) => Parser.Extract(pair.Left, pair.Right, ct)) .WithTrackingName(TrackingNames.Extraction); diff --git a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs index f8f3c371..4e52a980 100644 --- a/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/LenientNoMessageActionsGenerator.cs @@ -11,20 +11,21 @@ namespace NServiceBus.AzureFunctions.Analyzers.Tests; class LenientNoMessageActionsGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) - => FunctionEndpointGenerator.InitializeGenerator(context, - new FunctionEndpointGenerator.TriggerDefinition( - TriggerAttributeMetadataName: "Demo.Testing.TestTriggerAttribute", - AdditionalParameterTypes: ImmutableEquatableArray.Empty, - ProcessorTypeFullyQualified: "global::Demo.Testing.TestProcessor", - AddressExtraction: FunctionEndpointGenerator.AddressExtractionPolicy.FromNamedConstructorParameter("queueName"), - ConnectionSetting: FunctionEndpointGenerator.ConnectionSettingPolicy.FromNamedProperty("ConnSetting"), - AutoComplete: FunctionEndpointGenerator.AutoCompletePolicy.None, - RegistrationMethodFullyQualified: "global::Demo.Testing.TestFunctionManifestRegistration.Register", - ProcessMethodName: "Process", - Shape: FunctionEndpointGenerator.TriggerShape.RequiredAllowingAdditionalParameters( - FunctionEndpointGenerator.ParameterRole.TriggerMessage, - FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken))); + => FunctionEndpointGenerator.InitializeGenerator(context); + + sealed record LenientNoMessageActionsTriggerDefinition() : FunctionEndpointGenerator.TriggerDefinition( + TriggerAttributeMetadataName: "Demo.Testing.TestTriggerAttribute", + AdditionalParameterTypes: ImmutableEquatableArray.Empty, + ProcessorTypeFullyQualified: "global::Demo.Testing.TestProcessor", + AddressExtraction: FunctionEndpointGenerator.AddressExtractionPolicy.FromNamedConstructorParameter("queueName"), + ConnectionSetting: FunctionEndpointGenerator.ConnectionSettingPolicy.FromNamedProperty("ConnSetting"), + AutoComplete: FunctionEndpointGenerator.AutoCompletePolicy.None, + RegistrationMethodFullyQualified: "global::Demo.Testing.TestFunctionManifestRegistration.Register", + ProcessMethodName: "Process", + Shape: FunctionEndpointGenerator.TriggerShape.RequiredAllowingAdditionalParameters( + FunctionEndpointGenerator.ParameterRole.TriggerMessage, + FunctionEndpointGenerator.ParameterRole.FunctionContext, + FunctionEndpointGenerator.ParameterRole.CancellationToken)); internal static class TrackingNames { diff --git a/src/Tests.Analyzers/NoMessageActionsGenerator.cs b/src/Tests.Analyzers/NoMessageActionsGenerator.cs index 6bebefff..dbdd1638 100644 --- a/src/Tests.Analyzers/NoMessageActionsGenerator.cs +++ b/src/Tests.Analyzers/NoMessageActionsGenerator.cs @@ -11,20 +11,21 @@ namespace NServiceBus.AzureFunctions.Analyzers.Tests; class NoMessageActionsGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) - => FunctionEndpointGenerator.InitializeGenerator(context, - new FunctionEndpointGenerator.TriggerDefinition( - TriggerAttributeMetadataName: "Demo.Testing.TestTriggerAttribute", - AdditionalParameterTypes: ImmutableEquatableArray.Empty, - ProcessorTypeFullyQualified: "global::Demo.Testing.TestProcessor", - AddressExtraction: FunctionEndpointGenerator.AddressExtractionPolicy.FromNamedConstructorParameter("queueName"), - ConnectionSetting: FunctionEndpointGenerator.ConnectionSettingPolicy.FromNamedProperty("ConnSetting"), - AutoComplete: FunctionEndpointGenerator.AutoCompletePolicy.None, - RegistrationMethodFullyQualified: "global::Demo.Testing.TestFunctionManifestRegistration.Register", - ProcessMethodName: "Process", - Shape: FunctionEndpointGenerator.TriggerShape.Required( - FunctionEndpointGenerator.ParameterRole.TriggerMessage, - FunctionEndpointGenerator.ParameterRole.FunctionContext, - FunctionEndpointGenerator.ParameterRole.CancellationToken))); + => FunctionEndpointGenerator.InitializeGenerator(context); + + record NoMessageTriggerDefinition() : FunctionEndpointGenerator.TriggerDefinition( + TriggerAttributeMetadataName: "Demo.Testing.TestTriggerAttribute", + AdditionalParameterTypes: ImmutableEquatableArray.Empty, + ProcessorTypeFullyQualified: "global::Demo.Testing.TestProcessor", + AddressExtraction: FunctionEndpointGenerator.AddressExtractionPolicy.FromNamedConstructorParameter("queueName"), + ConnectionSetting: FunctionEndpointGenerator.ConnectionSettingPolicy.FromNamedProperty("ConnSetting"), + AutoComplete: FunctionEndpointGenerator.AutoCompletePolicy.None, + RegistrationMethodFullyQualified: "global::Demo.Testing.TestFunctionManifestRegistration.Register", + ProcessMethodName: "Process", + Shape: FunctionEndpointGenerator.TriggerShape.Required( + FunctionEndpointGenerator.ParameterRole.TriggerMessage, + FunctionEndpointGenerator.ParameterRole.FunctionContext, + FunctionEndpointGenerator.ParameterRole.CancellationToken)); internal static class TrackingNames { @@ -36,4 +37,4 @@ internal static class TrackingNames public static string[] All => [Extraction, Diagnostics, Functions, AssemblyClassName, Combined]; } -} +} \ No newline at end of file From 18f5c88522371899941ec4375f57b696b69c2d99 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 13:56:20 +0200 Subject: [PATCH 18/25] Comment for ourselves in the future --- .../FunctionEndpointGenerator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs index f808b61d..cf20ec36 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionEndpointGenerator.cs @@ -8,6 +8,10 @@ public sealed partial class FunctionEndpointGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) => InitializeGenerator(context); + // This method currently exists to proof that technically the generator pipeline can be extended with other trigger definitions + // without having to change the analyzer. In practice, we don't have any other trigger definitions at the moment, + // but there are tests that verify it would be possible to add more. There might still be some refactoring opportunities to make the extension story better + // but this is good enough for now. internal static void InitializeGenerator(IncrementalGeneratorInitializationContext context) where TDefinition : TriggerDefinition, new() { From 020fb12de047b0bf63fdabd006177ee071bc2fb7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 14:11:45 +0200 Subject: [PATCH 19/25] Reduce complexity in the configuration analyzer because now that we have a clear attribute we don't need compilation end anymore and therefore there are no races --- .../ConfigurationAnalyzer.cs | 132 ++++++------------ 1 file changed, 39 insertions(+), 93 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs index 3aa10eb8..5c6566f1 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs @@ -1,6 +1,5 @@ namespace NServiceBus.AzureFunctions.Analyzer; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -23,97 +22,50 @@ public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterCompilationStartAction(OnCompilationStart); - } - static void OnCompilationStart(CompilationStartAnalysisContext context) - { - var knownSymbols = new KnownSymbols( - context.Compilation.GetTypeByMetadataName(KnownTypeNames.EndpointConfigurationType), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.IServiceCollection), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.IConfiguration), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.IHostEnvironment), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.SendOptions), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.ReplyOptions), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.AzureServiceBusServerlessTransport), - context.Compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute)); - - var sendOnlyConfigureMethods = new ConcurrentDictionary(SymbolEqualityComparer.Default); - var deferredInvocations = new ConcurrentBag(); - - // Collect EndpointConfiguration invocations in qualifying methods. - // Context resolution is deferred to compilation end to avoid race conditions - // with sendOnlyConfigureMethods population from other syntax-node actions. - context.RegisterCodeBlockStartAction(blockStartContext => + context.RegisterCompilationStartAction(compilationStartContext => { - if (blockStartContext.OwningSymbol is IMethodSymbol method && HasSupportedConfigureMethodSignature(method, knownSymbols)) + var knownSymbols = new KnownSymbols( + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.EndpointConfigurationType), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.IServiceCollection), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.IConfiguration), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.IHostEnvironment), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.SendOptions), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.ReplyOptions), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.AzureServiceBusServerlessTransport), + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute)); + + compilationStartContext.RegisterCodeBlockStartAction(blockStartContext => { - if (HasSendOnlyEndpointAttribute(method, knownSymbols.SendOnlyEndpointAttribute)) + if (blockStartContext.OwningSymbol is not IMethodSymbol method) { - sendOnlyConfigureMethods.TryAdd(method.OriginalDefinition, true); + return; } - blockStartContext.RegisterSyntaxNodeAction( - nodeContext => CollectEndpointConfigurationInvocation(nodeContext, method, deferredInvocations), - SyntaxKind.InvocationExpression); - } - }); + if (!HasSupportedConfigureMethodSignature(method, knownSymbols)) + { + return; + } - // SendOptions/ReplyOptions: syntax-node-scoped, purely receiver-type checked. - context.RegisterSyntaxNodeAction( - nodeContext => AnalyzeSendAndReplyOptions(nodeContext, knownSymbols), - SyntaxKind.InvocationExpression); + var endpointContext = HasSendOnlyEndpointAttribute(method, knownSymbols.SendOnlyEndpointAttribute) ? EndpointConfigurationContext.SendOnlyEndpoint : EndpointConfigurationContext.AzureFunctionsEndpoint; - // Resolve deferred invocations after all syntax-node and code-block actions have completed. - // By this point sendOnlyConfigureMethods is fully populated — no race. - context.RegisterCompilationEndAction(endContext => - { - foreach (var deferred in deferredInvocations) - { - var endpointContext = sendOnlyConfigureMethods.ContainsKey(deferred.OwningMethod.OriginalDefinition) - ? EndpointConfigurationContext.SendOnlyEndpoint - : EndpointConfigurationContext.AzureFunctionsEndpoint; - - AnalyzeEndpointConfiguration( - deferred.Invocation, - deferred.SemanticModel, - endContext.ReportDiagnostic, - endpointContext, - knownSymbols, - endContext.CancellationToken); - } - }); - } + blockStartContext.RegisterSyntaxNodeAction(nodeContext => AnalyzeEndpointConfigurationInvocation(nodeContext, endpointContext, knownSymbols), SyntaxKind.InvocationExpression); + }); - static void CollectEndpointConfigurationInvocation( - SyntaxNodeAnalysisContext context, - IMethodSymbol owningMethod, - ConcurrentBag deferredInvocations) - { - if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax } invocationExpression) - { - return; - } - - deferredInvocations.Add(new DeferredInvocation(invocationExpression, owningMethod, context.SemanticModel)); + compilationStartContext.RegisterSyntaxNodeAction(nodeContext => AnalyzeSendAndReplyOptions(nodeContext, knownSymbols), SyntaxKind.InvocationExpression); + }); } - static void AnalyzeEndpointConfiguration( - InvocationExpressionSyntax invocationExpression, - SemanticModel semanticModel, - Action reportDiagnostic, - EndpointConfigurationContext endpointContext, - KnownSymbols knownSymbols, - CancellationToken cancellationToken) + static void AnalyzeEndpointConfigurationInvocation(SyntaxNodeAnalysisContext context, EndpointConfigurationContext endpointContext, KnownSymbols knownSymbols) { - if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression } invocationExpression) { return; } if (memberAccessExpression.Name.Identifier.ValueText == UseTransportMethodName) { - AnalyzeInvalidTransportConfiguration(invocationExpression, semanticModel, reportDiagnostic, endpointContext, knownSymbols, cancellationToken); + AnalyzeInvalidTransportConfiguration(invocationExpression, context.SemanticModel, context.ReportDiagnostic, endpointContext, knownSymbols, context.CancellationToken); return; } @@ -122,7 +74,7 @@ static void AnalyzeEndpointConfiguration( return; } - reportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticIds.InvalidEndpointConfigurationDescriptor, invocationExpression.GetLocation(), rule.ApiName, @@ -209,9 +161,7 @@ static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, Kno static string GetEndpointContextLabel(EndpointConfigurationContext endpointContext) => endpointContext == EndpointConfigurationContext.SendOnlyEndpoint ? SendOnlyEndpoints : AzureFunctionsEndpoints; - static string GetEndpointConfigurationReason( - InvalidEndpointConfigurationRule rule, - EndpointConfigurationContext endpointContext) + static string GetEndpointConfigurationReason(InvalidEndpointConfigurationRule rule, EndpointConfigurationContext endpointContext) { if (endpointContext == EndpointConfigurationContext.SendOnlyEndpoint && rule.SendOnlyReason is { } sendOnlyReason) @@ -248,12 +198,16 @@ static bool UsesAllowedTransport( const string UseTransportMethodName = "UseTransport"; const string AzureFunctionsEndpoints = "Azure Functions endpoints"; const string SendOnlyEndpoints = "Send-only endpoints"; - const string SendOnlyEndpointReason = "Send-only endpoints do not receive messages."; - readonly record struct DeferredInvocation( - InvocationExpressionSyntax Invocation, - IMethodSymbol OwningMethod, - SemanticModel SemanticModel); + enum EndpointConfigurationContext + { + AzureFunctionsEndpoint, + SendOnlyEndpoint + } + + readonly record struct InvalidEndpointConfigurationRule(string ApiName, string Reason, string? SendOnlyReason); + + readonly record struct InvalidSendOptionsRule(string Reason); readonly record struct KnownSymbols( INamedTypeSymbol? EndpointConfiguration, @@ -265,16 +219,6 @@ readonly record struct KnownSymbols( INamedTypeSymbol? AzureServiceBusServerlessTransport, INamedTypeSymbol? SendOnlyEndpointAttribute); - enum EndpointConfigurationContext - { - AzureFunctionsEndpoint, - SendOnlyEndpoint - } - - readonly record struct InvalidEndpointConfigurationRule(string ApiName, string Reason, string? SendOnlyReason); - - readonly record struct InvalidSendOptionsRule(string Reason); - static readonly Dictionary InvalidEndpointConfigurationMethods = new() { @@ -287,6 +231,8 @@ enum EndpointConfigurationContext ["OverrideLocalAddress"] = new("EndpointConfiguration.OverrideLocalAddress", "The endpoint address is determined by the trigger configuration.", SendOnlyEndpointReason) }; + const string SendOnlyEndpointReason = "Send-only endpoints do not receive messages."; + static readonly Dictionary InvalidSendAndReplyOptions = new() { From 9f1395f91b787c4431f4e966639438d08b6b6ac0 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 14:28:38 +0200 Subject: [PATCH 20/25] fix connection setting assignment --- ...nctionsHostApplicationBuilderExtensions.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs index abcea749..5626e1e7 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs @@ -24,10 +24,14 @@ public static void AddNServiceBusAzureServiceBusFunction(this FunctionsApplicati var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest); var transport = GetAzureServiceBusTransport(endpointConfiguration); - var resolvedConnectionSettingName = string.IsNullOrWhiteSpace(functionManifest.ConnectionSettingName) - ? functionManifest.ConnectionSettingName - : FunctionBindingExpression.Resolve(functionManifest.ConnectionSettingName, builder.Configuration); - transport.ConnectionName = resolvedConnectionSettingName; + if (!string.IsNullOrEmpty(functionManifest.ConnectionSettingName)) + { + // the connection name is resolved at runtime from the configuration and doesn't need to + // support binding expressions since Azure Functions also doesn't support them for trigger connection settings. + // The binding expression always wins + transport.ConnectionName = functionManifest.ConnectionSettingName; + } + builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointConfiguration.EndpointName); builder.Services.AddKeyedSingleton(functionManifest.Name, (_, _) => new AzureServiceBusMessageProcessor(transport, functionManifest.Name)); } @@ -42,12 +46,9 @@ public static void AddNServiceBusAzureServiceBusSendOnlyEndpoint(this FunctionsA builder.Services.AddAzureClientsCore(); var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildSendOnlyEndpointConfiguration(builder, sendOnlyEndpointManifest); - var transport = GetAzureServiceBusTransport(endpointConfiguration); + _ = GetAzureServiceBusTransport(endpointConfiguration); - if (!string.IsNullOrWhiteSpace(transport.ConnectionName)) - { - transport.ConnectionName = FunctionBindingExpression.Resolve(transport.ConnectionName, builder.Configuration); - } + // The connection name is resolved at runtime from the configuration and doesn't need to be assigned here builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointConfiguration.EndpointName); } From 496f20df8dd2133ac3d3734596efc060f5367139 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 14:36:40 +0200 Subject: [PATCH 21/25] Approve --- .../ApiApprovals.ApproveFunctionsComponentApi.approved.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt index efd0be32..5c495157 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt @@ -12,7 +12,7 @@ namespace NServiceBus public delegate void FunctionEndpointConfiguration(NServiceBus.EndpointConfiguration endpoint, Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.Extensions.Hosting.IHostEnvironment environment); public static class FunctionEndpointConfigurationBuilder { - public static NServiceBus.EndpointConfiguration BuildReceiveEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.FunctionManifest functionManifest, string sendOnlyEndpointApiName) { } + public static NServiceBus.EndpointConfiguration BuildReceiveEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.FunctionManifest functionManifest) { } public static NServiceBus.EndpointConfiguration BuildSendOnlyEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.SendOnlyEndpointManifest manifest) { } } public sealed class FunctionManifest : System.IEquatable From 135bcb978af0c60f9bceec620cfbecffad634f64 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 14:55:47 +0200 Subject: [PATCH 22/25] Support inferring the connection automatically --- src/IntegrationTestApp/ClientEndpoint.cs | 2 +- .../SendOnlyEndpointGenerator.Emitter.cs | 7 +++- .../SendOnlyEndpointGenerator.Parser.cs | 17 ++++++++++ ...nctionsHostApplicationBuilderExtensions.cs | 23 ++++++++----- .../AzureServiceBusServerlessTransport.cs | 4 +-- .../FunctionManifest.cs | 2 +- .../IConnectionSettingManifest.cs | 15 +++++++++ .../NServiceBusSendOnlyEndpointAttribute.cs | 10 ++++++ .../SendOnlyEndpointManifest.cs | 4 ++- ...veAzureServiceBusComponentApi.approved.txt | 1 - ....ApproveFunctionsComponentApi.approved.txt | 12 +++++-- ...s.GeneratesProjectComposition.approved.txt | 1 + ...atesMultipleSendOnlyEndpoints.approved.txt | 2 ++ ...OnlyEndpointInGlobalNamespace.approved.txt | 1 + ...sSendOnlyEndpointRegistration.approved.txt | 1 + ...ntWithAllAdditionalParameters.approved.txt | 1 + ...endOnlyEndpointWithConnection.approved.txt | 33 +++++++++++++++++++ ...intWithNoAdditionalParameters.approved.txt | 1 + .../SendOnlyEndpointGeneratorTests.cs | 10 +++++- src/Tests.Analyzers/TestSources.cs | 15 +++++++++ 20 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 src/NServiceBus.AzureFunctions.Common/IConnectionSettingManifest.cs create mode 100644 src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithConnection.approved.txt diff --git a/src/IntegrationTestApp/ClientEndpoint.cs b/src/IntegrationTestApp/ClientEndpoint.cs index ff1c6af9..a2ed0718 100644 --- a/src/IntegrationTestApp/ClientEndpoint.cs +++ b/src/IntegrationTestApp/ClientEndpoint.cs @@ -5,7 +5,7 @@ namespace IntegrationTestApp; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyEndpoint("client", Connection = "AzureWebJobsServiceBus")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { services.AddSingleton(new MyComponent("client")); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs index 228c8a8c..8e4ab8fe 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Emitter.cs @@ -38,8 +38,13 @@ public static void Emit(SourceProductionContext spc, ImmutableArray f.EndpointName, StringComparer.Ordinal)) { + var connectionSettingName = endpoint.ConnectionSettingName is not null + ? $"\"{endpoint.ConnectionSettingName}\"" + : "null"; + writer.WriteLine("yield return new global::NServiceBus.SendOnlyEndpointManifest("); writer.WriteLine($" \"{endpoint.EndpointName}\","); + writer.WriteLine($" {connectionSettingName},"); writer.WriteLine($" {GenerateConfigureMethodCall(endpoint.ConfigureMethod)},"); writer.WriteLine($" {endpoint.RegistrationMethodFullyQualified});"); } @@ -59,4 +64,4 @@ static string GenerateConfigureMethodCall(ConfigureMethodSpec configureMethod) return $"(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => {configureMethod.ContainingTypeFullyQualified}.{configureMethod.MethodName}({argumentList})"; } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs index d1839a1e..0843917f 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -91,8 +91,11 @@ static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendO parameterTypeNames[i] = method.Parameters[i].Type.Name.ToLowerInvariant(); } + var connectionSettingName = ExtractConnectionSettingName(sendOnlyEndpointAttribute); + return new SendOnlyEndpointSpec( endpointName, + connectionSettingName, sendOnlyEndpointDefinition.RegistrationMethodFullyQualified, new ConfigureMethodSpec( method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), @@ -100,6 +103,19 @@ static SendOnlyEndpointSpecs ExtractFromMethod(IMethodSymbol methodSymbol, SendO parameterTypeNames.ToImmutableEquatableArray())); } + static string? ExtractConnectionSettingName(AttributeData attribute) + { + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg is { Key: "Connection", Value.Value: string connectionName }) + { + return connectionName; + } + } + + return null; + } + static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, SendOnlyEndpointGeneratorKnownTypes knownTypes) => parameterType.IsAllowedConfigureMethodParameterType(knownTypes.IServiceCollection, knownTypes.IConfiguration, knownTypes.IHostEnvironment); } @@ -108,6 +124,7 @@ static bool IsAllowedConfigureMethodParameterType(ITypeSymbol parameterType, Sen internal sealed record SendOnlyEndpointSpec( string EndpointName, + string? ConnectionSettingName, string RegistrationMethodFullyQualified, ConfigureMethodSpec ConfigureMethod); diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs index 5626e1e7..25f46052 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusFunctionsHostApplicationBuilderExtensions.cs @@ -24,13 +24,7 @@ public static void AddNServiceBusAzureServiceBusFunction(this FunctionsApplicati var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildReceiveEndpointConfiguration(builder, functionManifest); var transport = GetAzureServiceBusTransport(endpointConfiguration); - if (!string.IsNullOrEmpty(functionManifest.ConnectionSettingName)) - { - // the connection name is resolved at runtime from the configuration and doesn't need to - // support binding expressions since Azure Functions also doesn't support them for trigger connection settings. - // The binding expression always wins - transport.ConnectionName = functionManifest.ConnectionSettingName; - } + ApplyConnectionSettingName(transport, functionManifest); builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointConfiguration.EndpointName); builder.Services.AddKeyedSingleton(functionManifest.Name, (_, _) => new AzureServiceBusMessageProcessor(transport, functionManifest.Name)); @@ -46,9 +40,9 @@ public static void AddNServiceBusAzureServiceBusSendOnlyEndpoint(this FunctionsA builder.Services.AddAzureClientsCore(); var endpointConfiguration = FunctionEndpointConfigurationBuilder.BuildSendOnlyEndpointConfiguration(builder, sendOnlyEndpointManifest); - _ = GetAzureServiceBusTransport(endpointConfiguration); + var transport = GetAzureServiceBusTransport(endpointConfiguration); - // The connection name is resolved at runtime from the configuration and doesn't need to be assigned here + ApplyConnectionSettingName(transport, sendOnlyEndpointManifest); builder.Services.AddNServiceBusEndpoint(endpointConfiguration, endpointConfiguration.EndpointName); } @@ -59,4 +53,15 @@ static AzureServiceBusServerlessTransport GetAzureServiceBusTransport(EndpointCo return transport ?? throw new InvalidOperationException($"Endpoint '{endpointConfiguration.EndpointName}' must be configured with an '{nameof(AzureServiceBusServerlessTransport)}'."); } + + static void ApplyConnectionSettingName(AzureServiceBusServerlessTransport transport, IConnectionSettingManifest manifest) + { + if (!string.IsNullOrEmpty(manifest.ConnectionSettingName)) + { + // the connection name is resolved at runtime from the configuration and doesn't need to + // support binding expressions since Azure Functions also doesn't support them for trigger connection settings. + // The binding expression always wins + transport.ConnectionName = manifest.ConnectionSettingName; + } + } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs index 8df3e651..5628c3a6 100644 --- a/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs +++ b/src/NServiceBus.AzureFunctions.AzureServiceBus/AzureServiceBusServerlessTransport.cs @@ -23,7 +23,7 @@ public class AzureServiceBusServerlessTransport : TransportDefinition { /// /// Creates a new transport using the supplied . The connection is - /// resolved from configuration during via . + /// resolved from configuration during . /// /// The topic topology describing how events are published and subscribed to. public AzureServiceBusServerlessTransport(TopicTopology topology) : base(TransportTransactionMode.ReceiveOnly, @@ -128,7 +128,7 @@ public HierarchyNamespaceOptions HierarchyNamespaceOptions /// AzureWebJobsServiceBus, matching the setting used by Service Bus triggers. /// /// The resolved value may be a connection string, or a configuration section containing a fullyQualifiedNamespace entry for token-credential authentication. - public string ConnectionName { get; set; } = DefaultServiceBusConnectionName; + internal string ConnectionName { get; set; } = DefaultServiceBusConnectionName; /// public override async Task Initialize( diff --git a/src/NServiceBus.AzureFunctions.Common/FunctionManifest.cs b/src/NServiceBus.AzureFunctions.Common/FunctionManifest.cs index d9bf9aa8..2492131b 100644 --- a/src/NServiceBus.AzureFunctions.Common/FunctionManifest.cs +++ b/src/NServiceBus.AzureFunctions.Common/FunctionManifest.cs @@ -13,4 +13,4 @@ namespace NServiceBus; /// The configuration key whose value resolves to the transport connection (for example, a connection string or fully-qualified namespace). /// Callback invoked to customize the endpoint configuration and its service registrations. /// Transport-specific callback that registers the endpoint with the Functions host builder. -public sealed record FunctionManifest(string Name, string Address, string ConnectionSettingName, FunctionEndpointConfiguration Configuration, Action Register); +public sealed record FunctionManifest(string Name, string Address, string ConnectionSettingName, FunctionEndpointConfiguration Configuration, Action Register) : IConnectionSettingManifest; \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Common/IConnectionSettingManifest.cs b/src/NServiceBus.AzureFunctions.Common/IConnectionSettingManifest.cs new file mode 100644 index 00000000..70054b17 --- /dev/null +++ b/src/NServiceBus.AzureFunctions.Common/IConnectionSettingManifest.cs @@ -0,0 +1,15 @@ +namespace NServiceBus; + +/// +/// Provides the name of the configuration key used to resolve the transport connection. +/// Implemented by manifest types produced by the source generator. +/// +/// The API surface might be changed between versions according to the needs of the source generator. +public interface IConnectionSettingManifest +{ + /// + /// The name of the application setting or configuration section that contains the transport connection details, + /// or to use the transport's default. + /// + string? ConnectionSettingName { get; } +} \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs index 55ff2b3f..4b56df46 100644 --- a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs +++ b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs @@ -11,4 +11,14 @@ public sealed class NServiceBusSendOnlyEndpointAttribute(string endpointName) : /// Gets the logical endpoint name. /// public string EndpointName { get; } = endpointName; + + /// + /// Gets or sets the name of the application setting or configuration section that contains the transport connection details. + /// When not set, the transport's default connection name is used. + /// + /// + /// This property holds the name of a configuration key, not the connection string value itself. + /// At runtime, the transport looks up this key in the application configuration to resolve the actual connection details. + /// + public string? Connection { get; set; } } \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs b/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs index 9052c485..68b97bcb 100644 --- a/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs +++ b/src/NServiceBus.AzureFunctions.Common/SendOnlyEndpointManifest.cs @@ -9,9 +9,11 @@ namespace NServiceBus; /// /// The API surface might be changed between versions according to the needs of the source generator. /// The endpoint name. +/// The name of the application setting or configuration section that contains the transport connection details, or to use the transport's default. /// Callback invoked to customize the endpoint configuration and its service registrations. /// Transport-specific callback that registers the endpoint with the Functions host builder. public sealed record SendOnlyEndpointManifest( string Name, + string? ConnectionSettingName, FunctionEndpointConfiguration Configuration, - Action Register); \ No newline at end of file + Action Register) : IConnectionSettingManifest; \ No newline at end of file diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt index 0b819f6d..29ccf8ed 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveAzureServiceBusComponentApi.approved.txt @@ -13,7 +13,6 @@ namespace NServiceBus { public AzureServiceBusServerlessTransport(NServiceBus.TopicTopology topology) { } public bool AutoForwardDeadLetteredMessagesToErrorQueue { get; set; } - public string ConnectionName { get; set; } public bool EnablePartitioning { get; set; } public int EntityMaximumSize { get; set; } public NServiceBus.Transport.AzureServiceBus.HierarchyNamespaceOptions HierarchyNamespaceOptions { get; set; } diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt index 5c495157..0a6dcd78 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt @@ -15,7 +15,7 @@ namespace NServiceBus public static NServiceBus.EndpointConfiguration BuildReceiveEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.FunctionManifest functionManifest) { } public static NServiceBus.EndpointConfiguration BuildSendOnlyEndpointConfiguration(Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder, NServiceBus.SendOnlyEndpointManifest manifest) { } } - public sealed class FunctionManifest : System.IEquatable + public sealed class FunctionManifest : NServiceBus.IConnectionSettingManifest, System.IEquatable { public FunctionManifest(string Name, string Address, string ConnectionSettingName, NServiceBus.FunctionEndpointConfiguration Configuration, System.Action Register) { } public string Address { get; init; } @@ -24,6 +24,10 @@ namespace NServiceBus public string Name { get; init; } public System.Action Register { get; init; } } + public interface IConnectionSettingManifest + { + string? ConnectionSettingName { get; } + } [System.AttributeUsage(System.AttributeTargets.Method, Inherited=false)] public sealed class NServiceBusFunctionAttribute : System.Attribute { @@ -33,12 +37,14 @@ namespace NServiceBus public sealed class NServiceBusSendOnlyEndpointAttribute : System.Attribute { public NServiceBusSendOnlyEndpointAttribute(string endpointName) { } + public string? Connection { get; set; } public string EndpointName { get; } } - public sealed class SendOnlyEndpointManifest : System.IEquatable + public sealed class SendOnlyEndpointManifest : NServiceBus.IConnectionSettingManifest, System.IEquatable { - public SendOnlyEndpointManifest(string Name, NServiceBus.FunctionEndpointConfiguration Configuration, System.Action Register) { } + public SendOnlyEndpointManifest(string Name, string? ConnectionSettingName, NServiceBus.FunctionEndpointConfiguration Configuration, System.Action Register) { } public NServiceBus.FunctionEndpointConfiguration Configuration { get; init; } + public string? ConnectionSettingName { get; init; } public string Name { get; init; } public System.Action Register { get; init; } } diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt index 8169c273..375eb24f 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt @@ -162,6 +162,7 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesProjectCompo { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt index afc73eeb..a5f1d89b 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesMultipleSendOnlyEndpoints.approved.txt @@ -25,10 +25,12 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesMultipleSend { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield return new global::NServiceBus.SendOnlyEndpointManifest( "sender", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.SenderEndpoint.ConfigureSender(endpointconfiguration, iconfiguration, ihostenvironment), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt index ae5d81dd..00e9b04d 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointInGlobalNamespace.approved.txt @@ -25,6 +25,7 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndp { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt index 779665dc..fe6c6f35 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointRegistration.approved.txt @@ -25,6 +25,7 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndp { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt index a37546d9..f2ba057e 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithAllAdditionalParameters.approved.txt @@ -25,6 +25,7 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndp { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithConnection.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithConnection.approved.txt new file mode 100644 index 00000000..71896e79 --- /dev/null +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithConnection.approved.txt @@ -0,0 +1,33 @@ +// == NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.SendOnlyEndpointGenerator/SendOnlyEndpointRegistration.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus.Generated; + +/// +/// Registrations for NServiceBus send-only endpoints in this assembly. +/// +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +[global::NServiceBus.AutoGeneratedFunctionRegistrationsAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndpointWithConnection_6424a5930f5a14a8 +{ + /// + /// Gets send-only endpoint manifests for NServiceBus endpoints in this assembly. + /// + public static global::System.Collections.Generic.IEnumerable + GetSendOnlyEndpointManifests() + { + yield return new global::NServiceBus.SendOnlyEndpointManifest( + "client", + "MyCustomConnection", + (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration, iservicecollection), + global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); + yield break; + } +} \ No newline at end of file diff --git a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt index 9a6f4bdd..fcea13bd 100644 --- a/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/SendOnlyEndpointGeneratorTests.GeneratesSendOnlyEndpointWithNoAdditionalParameters.approved.txt @@ -25,6 +25,7 @@ public static class GeneratedSendOnlyEndpointRegistrations_GeneratesSendOnlyEndp { yield return new global::NServiceBus.SendOnlyEndpointManifest( "client", + null, (endpointconfiguration, iservicecollection, iconfiguration, ihostenvironment) => global::Demo.ClientEndpoint.ConfigureClient(endpointconfiguration), global::NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions.AddNServiceBusAzureServiceBusSendOnlyEndpoint); yield break; diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index 8c726ac5..7e29bcdb 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -48,6 +48,14 @@ public void GeneratesNoRegistrationsWhenNoSendOnlyEndpoints() => .Approve() .AssertRunsAreEqual(); + [Test] + public void GeneratesSendOnlyEndpointWithConnection() => + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(TestSources.ValidSendOnlyEndpointWithConnection) + .Run() + .Approve() + .AssertRunsAreEqual(); + [Test] public void GeneratesMultipleSendOnlyEndpoints() => SourceGeneratorTest.ForIncrementalGenerator() @@ -201,4 +209,4 @@ static Diagnostic GetInvalidSendOnlyEndpointMethodDiagnostic(string source) } #endregion -} +} \ No newline at end of file diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index 716b8344..5a284032 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -226,4 +226,19 @@ public static class SomeClass public static void DoSomething() { } } """; + + public const string ValidSendOnlyEndpointWithConnection = """ + using NServiceBus; + namespace Demo; + + using Microsoft.Extensions.DependencyInjection; + + public static class ClientEndpoint + { + [NServiceBusSendOnlyEndpoint("client", Connection = "MyCustomConnection")] + public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) + { + } + } + """; } \ No newline at end of file From 41cc7cd45fd14f93cb48316ab29fc390c23ab2c5 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 15:17:31 +0200 Subject: [PATCH 23/25] Trigger --- .../NServiceBusSendOnlyEndpointAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs index 4b56df46..902a779e 100644 --- a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs +++ b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs @@ -14,7 +14,7 @@ public sealed class NServiceBusSendOnlyEndpointAttribute(string endpointName) : /// /// Gets or sets the name of the application setting or configuration section that contains the transport connection details. - /// When not set, the transport's default connection name is used. + /// When not set, the trigger's default connection name is used. /// /// /// This property holds the name of a configuration key, not the connection string value itself. From 3684839753432f5f8bbe1ff531822f61526f1905 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 15:42:36 +0200 Subject: [PATCH 24/25] Rename NServiceBusSendOnlyEndpointAttribute to NServiceBusSendOnlyFunctionAttribute and update references --- src/IntegrationTestApp/ClientEndpoint.cs | 2 +- .../ConfigurationAnalyzer.cs | 2 +- .../FunctionCompositionGenerator.cs | 2 +- .../KnownTypeNames.cs | 5 +---- .../SendOnlyEndpointGenerator.Parser.cs | 2 +- .../SendOnlyEndpointGenerator.cs | 2 +- .../FunctionEndpointConfigurationBuilder.cs | 2 +- ....cs => NServiceBusSendOnlyFunctionAttribute.cs} | 2 +- ...ovals.ApproveFunctionsComponentApi.approved.txt | 4 ++-- ...rTests.GeneratesProjectComposition.approved.txt | 2 +- src/Tests.Analyzers/ConfigurationAnalyzerTests.cs | 6 +++--- .../SendOnlyEndpointGeneratorTests.cs | 12 ++++++------ src/Tests.Analyzers/TestSources.cs | 14 +++++++------- 13 files changed, 27 insertions(+), 30 deletions(-) rename src/NServiceBus.AzureFunctions.Common/{NServiceBusSendOnlyEndpointAttribute.cs => NServiceBusSendOnlyFunctionAttribute.cs} (94%) diff --git a/src/IntegrationTestApp/ClientEndpoint.cs b/src/IntegrationTestApp/ClientEndpoint.cs index a2ed0718..87952c7c 100644 --- a/src/IntegrationTestApp/ClientEndpoint.cs +++ b/src/IntegrationTestApp/ClientEndpoint.cs @@ -5,7 +5,7 @@ namespace IntegrationTestApp; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client", Connection = "AzureWebJobsServiceBus")] + [NServiceBusSendOnlyFunction("client", Connection = "AzureWebJobsServiceBus")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { services.AddSingleton(new MyComponent("client")); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs index 5c6566f1..2dbabd8f 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs @@ -33,7 +33,7 @@ public override void Initialize(AnalysisContext context) compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.SendOptions), compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.ReplyOptions), compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.AzureServiceBusServerlessTransport), - compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute)); + compilationStartContext.Compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyFunctionAttribute)); compilationStartContext.RegisterCodeBlockStartAction(blockStartContext => { diff --git a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs index 3905b7db..578c7e6f 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs @@ -23,7 +23,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var hasLocalSendOnlyEndpoints = context.SyntaxProvider .ForAttributeWithMetadataName( - KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, + KnownTypeNames.NServiceBusSendOnlyFunctionAttribute, static (node, _) => node is MethodDeclarationSyntax, static (_, _) => true) .Collect() diff --git a/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs b/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs index dfb97d02..c0ebbeab 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/KnownTypeNames.cs @@ -5,20 +5,17 @@ static class KnownTypeNames public const string FunctionAttribute = "Microsoft.Azure.Functions.Worker.FunctionAttribute"; public const string FunctionContext = "Microsoft.Azure.Functions.Worker.FunctionContext"; public const string NServiceBusFunctionAttribute = "NServiceBus.NServiceBusFunctionAttribute"; - public const string NServiceBusSendOnlyEndpointAttribute = "NServiceBus.NServiceBusSendOnlyEndpointAttribute"; + public const string NServiceBusSendOnlyFunctionAttribute = "NServiceBus.NServiceBusSendOnlyFunctionAttribute"; public const string CancellationToken = "System.Threading.CancellationToken"; public const string EndpointConfigurationType = "NServiceBus.EndpointConfiguration"; public const string IHandleMessages = "NServiceBus.IHandleMessages`1"; public const string SendOptions = "NServiceBus.SendOptions"; public const string ReplyOptions = "NServiceBus.ReplyOptions"; public const string AzureServiceBusServerlessTransport = "NServiceBus.AzureServiceBusServerlessTransport"; - public const string ActionOfT = "System.Action`1"; - public const string ActionOfT1T2 = "System.Action`2"; public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; public const string IConfiguration = "Microsoft.Extensions.Configuration.IConfiguration"; public const string IHostEnvironment = "Microsoft.Extensions.Hosting.IHostEnvironment"; public const string AzureServiceBusFunctionsHostApplicationBuilderExtensions = "NServiceBus.Configuration.AdvancedExtensibility.AzureServiceBusFunctionsHostApplicationBuilderExtensions"; - public const string SendOnlyEndpointManifest = "NServiceBus.SendOnlyEndpointManifest"; public const string AddNServiceBusFunctions = "AddNServiceBusFunctions"; public const string AddNServiceBusAzureServiceBusFunction = "AddNServiceBusAzureServiceBusFunction"; public const string AddNServiceBusAzureServiceBusSendOnlyEndpoint = "AddNServiceBusAzureServiceBusSendOnlyEndpoint"; diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs index 0843917f..dffdcb22 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.Parser.cs @@ -155,7 +155,7 @@ readonly struct SendOnlyEndpointGeneratorKnownTypes( public static bool TryGet(Compilation compilation, out SendOnlyEndpointGeneratorKnownTypes knownTypes) { - var sendOnlyEndpointAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyEndpointAttribute); + var sendOnlyEndpointAttribute = compilation.GetTypeByMetadataName(KnownTypeNames.NServiceBusSendOnlyFunctionAttribute); var endpointConfiguration = compilation.GetTypeByMetadataName(KnownTypeNames.EndpointConfigurationType); var iServiceCollection = compilation.GetTypeByMetadataName(KnownTypeNames.IServiceCollection); var iconfiguration = compilation.GetTypeByMetadataName(KnownTypeNames.IConfiguration); diff --git a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs index 3869c1d1..b06cf03b 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/SendOnlyEndpointGenerator.cs @@ -10,7 +10,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { var extractionCandidates = context.SyntaxProvider .ForAttributeWithMetadataName( - KnownTypeNames.NServiceBusSendOnlyEndpointAttribute, + KnownTypeNames.NServiceBusSendOnlyFunctionAttribute, predicate: static (node, _) => node is MethodDeclarationSyntax, transform: static (ctx, _) => ctx); diff --git a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs index 4135eff9..22162d99 100644 --- a/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs +++ b/src/NServiceBus.AzureFunctions.Common/FunctionEndpointConfigurationBuilder.cs @@ -35,7 +35,7 @@ public static EndpointConfiguration BuildReceiveEndpointConfiguration( if (endpointConfiguration.IsSendOnly) { - throw new InvalidOperationException($"Functions can't be send-only endpoints, use [{typeof(NServiceBusSendOnlyEndpointAttribute)}] to create send-only endpoints."); + throw new InvalidOperationException($"Functions can't be send-only endpoints, use [{typeof(NServiceBusSendOnlyFunctionAttribute)}] to create send-only endpoints."); } var resolvedAddress = FunctionBindingExpression.Resolve(functionManifest.Address, builder.Configuration); diff --git a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyFunctionAttribute.cs similarity index 94% rename from src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs rename to src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyFunctionAttribute.cs index 902a779e..e8b08d86 100644 --- a/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyEndpointAttribute.cs +++ b/src/NServiceBus.AzureFunctions.Common/NServiceBusSendOnlyFunctionAttribute.cs @@ -5,7 +5,7 @@ namespace NServiceBus; /// The source generator produces endpoint registration code from methods marked with this attribute. /// [AttributeUsage(AttributeTargets.Method, Inherited = false)] -public sealed class NServiceBusSendOnlyEndpointAttribute(string endpointName) : Attribute +public sealed class NServiceBusSendOnlyFunctionAttribute(string endpointName) : Attribute { /// /// Gets the logical endpoint name. diff --git a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt index 0a6dcd78..ed90791c 100644 --- a/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt +++ b/src/NServiceBus.AzureFunctions.Tests/ApprovalFiles/ApiApprovals.ApproveFunctionsComponentApi.approved.txt @@ -34,9 +34,9 @@ namespace NServiceBus public NServiceBusFunctionAttribute() { } } [System.AttributeUsage(System.AttributeTargets.Method, Inherited=false)] - public sealed class NServiceBusSendOnlyEndpointAttribute : System.Attribute + public sealed class NServiceBusSendOnlyFunctionAttribute : System.Attribute { - public NServiceBusSendOnlyEndpointAttribute(string endpointName) { } + public NServiceBusSendOnlyFunctionAttribute(string endpointName) { } public string? Connection { get; set; } public string EndpointName { get; } } diff --git a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt index 375eb24f..abcaff1d 100644 --- a/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt +++ b/src/Tests.Analyzers/ApprovalFiles/FunctionCompositionGeneratorTests.GeneratesProjectComposition.approved.txt @@ -35,7 +35,7 @@ using Microsoft.Extensions.DependencyInjection; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { } diff --git a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs index ac588f9a..50b4d6e5 100644 --- a/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs +++ b/src/Tests.Analyzers/ConfigurationAnalyzerTests.cs @@ -143,7 +143,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration) { endpointConfiguration.UseTransport(new AzureServiceBusServerlessTransport(TopicTopology.Default)); @@ -183,7 +183,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration) { [|endpointConfiguration.{{configuration}}|]; @@ -203,7 +203,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { [|endpointConfiguration.MakeInstanceUniquelyAddressable("instance")|]; diff --git a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs index 7e29bcdb..89721972 100644 --- a/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs +++ b/src/Tests.Analyzers/SendOnlyEndpointGeneratorTests.cs @@ -75,7 +75,7 @@ namespace Demo; public class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public void ConfigureClient(EndpointConfiguration endpointConfiguration) { } @@ -98,7 +98,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void WrongName(EndpointConfiguration endpointConfiguration) { } @@ -118,7 +118,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(string wrongParam) { } @@ -138,7 +138,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient() { } @@ -158,7 +158,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, string invalidParam) { } @@ -178,7 +178,7 @@ namespace Demo; public class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public void WrongName(string invalidParam) { } diff --git a/src/Tests.Analyzers/TestSources.cs b/src/Tests.Analyzers/TestSources.cs index 5a284032..bae3e2c0 100644 --- a/src/Tests.Analyzers/TestSources.cs +++ b/src/Tests.Analyzers/TestSources.cs @@ -134,7 +134,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { } @@ -147,7 +147,7 @@ public static void ConfigureClient(EndpointConfiguration endpointConfiguration, public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { } @@ -164,7 +164,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient( EndpointConfiguration endpointConfiguration, IServiceCollection services, @@ -181,7 +181,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration) { } @@ -198,7 +198,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client")] + [NServiceBusSendOnlyFunction("client")] public static void ConfigureClient( EndpointConfiguration endpointConfiguration, IServiceCollection services) @@ -208,7 +208,7 @@ public static void ConfigureClient( public static class SenderEndpoint { - [NServiceBusSendOnlyEndpoint("sender")] + [NServiceBusSendOnlyFunction("sender")] public static void ConfigureSender( EndpointConfiguration endpointConfiguration, IConfiguration configuration, @@ -235,7 +235,7 @@ namespace Demo; public static class ClientEndpoint { - [NServiceBusSendOnlyEndpoint("client", Connection = "MyCustomConnection")] + [NServiceBusSendOnlyFunction("client", Connection = "MyCustomConnection")] public static void ConfigureClient(EndpointConfiguration endpointConfiguration, IServiceCollection services) { } From f13ad7df4646ab697b2b285dbdcf91872b7c9b9b Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 3 Jun 2026 23:17:04 +0200 Subject: [PATCH 25/25] Update src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs Co-authored-by: David Boike --- src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs index f11da031..65c34721 100644 --- a/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs +++ b/src/NServiceBus.AzureFunctions.Analyzer/DiagnosticIds.cs @@ -103,7 +103,7 @@ static class DiagnosticIds internal static readonly DiagnosticDescriptor InvalidSendOnlyEndpointMethodDescriptor = new( id: InvalidSendOnlyEndpointMethod, title: "Invalid NServiceBus send-only endpoint method", - messageFormat: "Method '{0}' is not a valid NServiceBus send-only endpoint: {1}", + messageFormat: "Method '{0}' defines an NServiceBus send-only endpoint but is not correctly implemented: {1}", category: "NServiceBus.AzureFunctions", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true);