From 0affa25e9801299457291ac4932a7f12bf50cb7f Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 14 May 2026 09:43:43 +0200 Subject: [PATCH] Honor `[Subscribe(With = ...)]` in source-generated subscriptions --- .../FileBuilders/TypeFileBuilderBase.cs | 8 ++ .../Inspectors/ObjectTypeInspector.cs | 99 ++++++++++++- .../src/Types.Analyzers/Models/Resolver.cs | 13 +- .../Types.Analyzers/WellKnownAttributes.cs | 1 + .../IntegrationTests.cs | 45 ++++++ .../Subscription.cs | 25 ++++ .../IntegrationTests.Schema_Snapshot.snap | 6 + .../Types.Analyzers.Tests/OperationTests.cs | 52 +++++++ ...ethod_Does_Not_Suppress_Public_Resolver.md | 118 +++++++++++++++ ...h_Subscribe_With_Excludes_Stream_Method.md | 136 ++++++++++++++++++ 10 files changed, 496 insertions(+), 7 deletions(-) create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Subscription.cs create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver.md create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_With_Subscribe_With_Excludes_Stream_Method.md diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index 68a8c9ad73b..d2d3a2fe655 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -295,6 +295,14 @@ private void WriteResolverBindingExtendsWith( WriteResolverBindingDescriptor(type, resolver); + if (resolver.SubscribeWith is not null) + { + Writer.WriteIndentedLine( + "configuration.SubscribeWith = \"{0}\";", + GeneratorUtils.EscapeForStringLiteral(resolver.SubscribeWith)); + Writer.WriteIndentedLine("configuration.SourceType = context.ThisType;"); + } + if (resolver.Kind is ResolverKind.BatchResolver) { // For batch resolvers, the return type is a list (e.g. List). diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs index 86ad03c14b2..80c8d09af47 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ObjectTypeInspector.cs @@ -57,6 +57,12 @@ public bool TryHandle(GeneratorSyntaxContext context, [NotNullWhen(true)] out Sy Resolver? nodeResolver = null; var i = 0; + CollectSubscribeWithNames( + members, + includeInternalMembers, + out var subscribeSourceNames, + out var subscribeWithLookup); + foreach (var member in members) { if (member.IsIgnored()) @@ -83,13 +89,23 @@ or Accessibility.ProtectedOrInternal continue; } + // Skip methods that are referenced by a sibling [Subscribe(With = nameof(...))] attribute. + // These methods produce the event stream and must not be exposed as their own GraphQL fields. + if (subscribeSourceNames?.Contains(methodSymbol.Name) == true) + { + continue; + } + if (!isOperationType && hasNodeResolverAttribute) { nodeResolver = CreateNodeResolver(context, classSymbol, methodSymbol, ref diagnostics); continue; } - resolvers[i++] = CreateResolver(context, classSymbol, methodSymbol); + string? subscribeWith = null; + subscribeWithLookup?.TryGetValue(methodSymbol, out subscribeWith); + + resolvers[i++] = CreateResolver(context, classSymbol, methodSymbol, subscribeWith); continue; } @@ -277,14 +293,16 @@ private static bool IsOperationType( private static Resolver CreateResolver( GeneratorSyntaxContext context, INamedTypeSymbol resolverType, - IMethodSymbol resolverMethod) - => CreateResolver(context.SemanticModel.Compilation, resolverType, resolverMethod); + IMethodSymbol resolverMethod, + string? subscribeWith = null) + => CreateResolver(context.SemanticModel.Compilation, resolverType, resolverMethod, subscribeWith: subscribeWith); public static Resolver CreateResolver( Compilation compilation, INamedTypeSymbol resolverType, IMethodSymbol resolverMethod, - string? resolverTypeName = null) + string? resolverTypeName = null, + string? subscribeWith = null) { var parameters = resolverMethod.Parameters; var buffer = new ResolverParameter[parameters.Length]; @@ -324,7 +342,78 @@ public static Resolver CreateResolver( ? ResolverKind.BatchResolver : compilation.IsConnectionType(resolverMethod.ReturnType) ? ResolverKind.ConnectionResolver - : ResolverKind.Default); + : ResolverKind.Default, + subscribeWith: subscribeWith); + } + + private static void CollectSubscribeWithNames( + ImmutableArray members, + bool includeInternalMembers, + out HashSet? subscribeSourceNames, + out Dictionary? subscribeWithLookup) + { + subscribeSourceNames = null; + subscribeWithLookup = null; + + foreach (var member in members) + { + if (member.IsIgnored()) + { + continue; + } + + if (member is not IMethodSymbol { MethodKind: MethodKind.Ordinary } method) + { + continue; + } + + if (!IsVisibleResolverMember(method, includeInternalMembers)) + { + continue; + } + + if (method.Skip()) + { + continue; + } + + if (!TryGetSubscribeWith(method, out var with)) + { + continue; + } + + subscribeSourceNames ??= []; + subscribeWithLookup ??= new Dictionary(SymbolEqualityComparer.Default); + subscribeSourceNames.Add(with); + subscribeWithLookup[method] = with; + } + } + + private static bool TryGetSubscribeWith( + IMethodSymbol methodSymbol, + [NotNullWhen(true)] out string? with) + { + foreach (var attribute in methodSymbol.GetAttributes()) + { + if (attribute.AttributeClass?.ToDisplayString() != SubscribeAttribute) + { + continue; + } + + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg.Key == "With" + && namedArg.Value.Value is string value + && !string.IsNullOrEmpty(value)) + { + with = value; + return true; + } + } + } + + with = null; + return false; } private static Resolver CreateNodeResolver( diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs index 1b400346696..af042da6852 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/Resolver.cs @@ -18,7 +18,8 @@ public Resolver( ImmutableArray bindings, SchemaTypeReference schemaTypeRef, ResolverKind kind = ResolverKind.Default, - FieldFlags flags = FieldFlags.None) + FieldFlags flags = FieldFlags.None, + string? subscribeWith = null) { TypeName = typeName; Member = member; @@ -31,6 +32,7 @@ public Resolver( Bindings = bindings; Kind = kind; Flags = flags; + SubscribeWith = subscribeWith; if (description is MethodDescription m && parameters.Length == m.ParameterDescriptions.Length) { @@ -97,6 +99,12 @@ public bool IsPure public ImmutableArray DescriptorAttributes { get; } + /// + /// The name of the sibling method that produces the subscription event stream + /// when this resolver is annotated with [Subscribe(With = nameof(...))]. + /// + public string? SubscribeWith { get; } + public Resolver WithSchemaTypeName(SchemaTypeReference schemaTypeRef) => new Resolver( TypeName, @@ -108,5 +116,6 @@ public Resolver WithSchemaTypeName(SchemaTypeReference schemaTypeRef) Bindings, schemaTypeRef, Kind, - Flags); + Flags, + SubscribeWith); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs index d62a1805693..28208bd4833 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs @@ -19,6 +19,7 @@ public static class WellKnownAttributes public const string QueryAttribute = "HotChocolate.QueryAttribute"; public const string MutationAttribute = "HotChocolate.MutationAttribute"; public const string SubscriptionAttribute = "HotChocolate.SubscriptionAttribute"; + public const string SubscribeAttribute = "HotChocolate.Types.SubscribeAttribute"; public const string NodeResolverAttribute = "HotChocolate.Types.Relay.NodeResolverAttribute"; public const string ParentAttribute = "HotChocolate.ParentAttribute"; public const string EventMessageAttribute = "HotChocolate.EventMessageAttribute"; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/IntegrationTests.cs index af23b5725d8..d81eee1765a 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/IntegrationTests.cs @@ -18,6 +18,51 @@ public async Task Schema_Snapshot() .MatchSnapshotAsync(); } + [Fact] + public async Task Subscription_With_Subscribe_With_Delivers_Message_From_Stream() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQLServer() + .AddIntegrationTestTypes() + .AddPagingArguments() + .BuildRequestExecutorAsync(); + + // act + await using var subscriptionResult = await executor.ExecuteAsync( + "subscription { onProductAdded(categoryId: 42) }"); + + // assert + var stream = subscriptionResult.ExpectResponseStream(); + await foreach (var result in stream.ReadResultsAsync()) + { + result.MatchInlineSnapshot( + """ + { + "data": { + "onProductAdded": 42 + } + } + """); + break; + } + } + + [Fact] + public async Task Subscription_With_Public_Subscribe_Source_Is_Not_Exposed_As_Field() + { + var schema = await new ServiceCollection() + .AddGraphQLServer() + .AddIntegrationTestTypes() + .AddPagingArguments() + .BuildSchemaAsync(); + + var subscription = schema.Types.GetType("Subscription"); + Assert.Equal( + ["onProductAdded", "onProductPriceChanged"], + subscription.Fields.Where(f => !f.IsIntrospectionField).Select(f => f.Name).ToArray()); + } + [Fact] public async Task Maps_NullOrdering_From_PagingOptions_To_PagingArguments() { diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Subscription.cs b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Subscription.cs new file mode 100644 index 00000000000..43c602e216d --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Subscription.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.Types; + +[SubscriptionType] +public static partial class Subscription +{ + [Subscribe(With = nameof(SubscribeToOnProductAdded))] + public static Task OnProductAdded([EventMessage] int productId) + => Task.FromResult(productId); + + private static async IAsyncEnumerable SubscribeToOnProductAdded(int categoryId) + { + await Task.Yield(); + yield return categoryId; + } + + [Subscribe(With = nameof(SubscribeToOnProductPriceChanged))] + public static Task OnProductPriceChanged([EventMessage] int newPrice) + => Task.FromResult(newPrice); + + public static async IAsyncEnumerable SubscribeToOnProductPriceChanged(int productId) + { + await Task.Yield(); + yield return productId; + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot.snap b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot.snap index e97c1a7e56b..44ae9d6fe1e 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot.snap +++ b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot.snap @@ -1,5 +1,6 @@ schema { query: Query + subscription: Subscription } type Query { @@ -60,6 +61,11 @@ type Query { issue8057Entity(id: ID!): Issue8057Entity @cost(weight: "10") } +type Subscription { + onProductAdded(categoryId: Int!): Int! @cost(weight: "10") + onProductPriceChanged(productId: Int!): Int! @cost(weight: "10") +} + type Book implements Product { title: String! id: String! diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs index 1f9011096ec..f6004562495 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/OperationTests.cs @@ -353,6 +353,58 @@ public static int GetTest(string arg) """).MatchMarkdownAsync(); } + [Fact] + public async Task Subscription_With_Subscribe_With_Excludes_Stream_Method() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System.Collections.Generic; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [SubscriptionType] + public static partial class Subscription + { + [Subscribe(With = nameof(SubscribeToOnProductAdded))] + public static Task OnProductAdded([EventMessage] int productId) + => Task.FromResult(productId); + + private static async IAsyncEnumerable SubscribeToOnProductAdded(int categoryId) + { + await Task.Yield(); + yield return categoryId; + } + } + """).MatchMarkdownAsync(); + } + + [Fact] + public async Task Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver() + { + await TestHelper.GetGeneratedSourceSnapshot( + """ + using System.Collections.Generic; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + namespace TestNamespace; + + [SubscriptionType] + public static partial class Subscription + { + public static int OnFoo() => 42; + + [GraphQLIgnore] + [Subscribe(With = nameof(OnFoo))] + public static Task NotARealResolver() => Task.FromResult(0); + } + """).MatchMarkdownAsync(); + } + [Fact] public async Task Lookup_With_Generic_ID_Attribute() { diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver.md new file mode 100644 index 00000000000..5989668d92b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver.md @@ -0,0 +1,118 @@ +# Subscription_Ignored_Method_Does_Not_Suppress_Public_Resolver + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Subscription", + global::HotChocolate.Types.OperationTypeNames.Subscription, + () => global::TestNamespace.Subscription.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Subscription)), + HotChocolate.Language.OperationType.Subscription)); + return builder; + } + } +} + +``` + +## Subscription.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class Subscription + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var extension = descriptor.Extend(); + var configuration = extension.Configuration; + var thisType = typeof(global::TestNamespace.Subscription); + var bindingResolver = extension.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + HotChocolate.Internal.ConfigurationHelper.ApplyConfiguration( + extension.Context, + descriptor, + null, + new global::HotChocolate.Types.SubscriptionTypeAttribute()); + configuration.ConfigurationsAreApplied = true; + + var naming = descriptor.Extend().Context.Naming; + + descriptor + .Field(naming.GetMemberName("OnFoo", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Type = global::HotChocolate.Types.Descriptors.TypeReference.Create( + typeInspector.GetTypeRef(typeof(int), HotChocolate.Types.TypeContext.Output), + new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.NamedTypeNode("int"))); + configuration.ResultType = typeof(int); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.OnFoo(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates OnFoo() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: OnFoo); + } + + private global::System.Object? OnFoo(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Subscription.OnFoo(); + return result; + } + } + } +} + + +``` diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_With_Subscribe_With_Excludes_Stream_Method.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_With_Subscribe_With_Excludes_Stream_Method.md new file mode 100644 index 00000000000..b26090c51b5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/OperationTests.Subscription_With_Subscribe_With_Excludes_Stream_Method.md @@ -0,0 +1,136 @@ +# Subscription_With_Subscribe_With_Excludes_Stream_Method + +## HotChocolateTypeModule.735550c.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class TestsTypesRequestExecutorBuilderExtensions + { + public static IRequestExecutorBuilder AddTestsTypes(this IRequestExecutorBuilder builder) + { + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Subscription", + global::HotChocolate.Types.OperationTypeNames.Subscription, + () => global::TestNamespace.Subscription.Initialize)); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Subscription)), + HotChocolate.Language.OperationType.Subscription)); + return builder; + } + } +} + +``` + +## Subscription.WaAdMHmlGJHjtEI4nqY7WA.hc.g.cs + +```csharp +// + +#nullable enable +#pragma warning disable + +using System; +using System.Runtime.CompilerServices; +using HotChocolate; +using HotChocolate.Types; +using HotChocolate.Execution.Configuration; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Internal; + +namespace TestNamespace +{ + public static partial class Subscription + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var extension = descriptor.Extend(); + var configuration = extension.Configuration; + var thisType = typeof(global::TestNamespace.Subscription); + var bindingResolver = extension.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + HotChocolate.Internal.ConfigurationHelper.ApplyConfiguration( + extension.Context, + descriptor, + null, + new global::HotChocolate.Types.SubscriptionTypeAttribute()); + configuration.ConfigurationsAreApplied = true; + + var naming = descriptor.Extend().Context.Naming; + + descriptor + .Field(naming.GetMemberName("OnProductAdded", global::HotChocolate.Types.MemberKind.ObjectField)) + .ExtendWith(static (field, context) => + { + var configuration = field.Configuration; + var typeInspector = field.Context.TypeInspector; + var bindingResolver = field.Context.ParameterBindingResolver; + var naming = field.Context.Naming; + + configuration.Type = global::HotChocolate.Types.Descriptors.TypeReference.Create( + typeInspector.GetTypeRef(typeof(int), HotChocolate.Types.TypeContext.Output), + new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.NamedTypeNode("int"))); + configuration.SubscribeWith = "SubscribeToOnProductAdded"; + configuration.SourceType = context.ThisType; + configuration.ResultType = typeof(int); + + configuration.SetSourceGeneratorFlags(); + + configuration.Member = context.ThisType.GetMethod( + "OnProductAdded", + global::HotChocolate.Utilities.ReflectionUtils.StaticMemberFlags, + new global::System.Type[] + { + typeof(int) + })!; + + var fieldDescriptor = global::HotChocolate.Types.Descriptors.ObjectFieldDescriptor.From(field.Context, configuration); + HotChocolate.Internal.ConfigurationHelper.ApplyConfiguration( + field.Context, + fieldDescriptor, + configuration.Member, + new global::HotChocolate.Types.SubscribeAttribute() { With = "SubscribeToOnProductAdded" }); + configuration.ConfigurationsAreApplied = true; + fieldDescriptor.CreateConfiguration(); + + configuration.Resolvers = context.Resolvers.OnProductAdded(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates OnProductAdded() + => new global::HotChocolate.Resolvers.FieldResolverDelegates(resolver: OnProductAdded); + + private async global::System.Threading.Tasks.ValueTask OnProductAdded(global::HotChocolate.Resolvers.IResolverContext context) + { + var args0 = context.GetScopedState(global::HotChocolate.WellKnownContextData.EventMessage); + var result = await global::TestNamespace.Subscription.OnProductAdded(args0); + return result; + } + } + } +} + + +```