From 1c071ac8e0cb4d71000f05f965d664574fe6a07f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Feb 2026 20:07:23 +0000 Subject: [PATCH 1/2] Fix #6513 register interface-derived object types --- .../Inspectors/ClassBaseClassInspector.cs | 9 ++ .../TypeModuleSyntaxGeneratorTests.cs | 36 ++++++ ...tance_Registers_Derived_Implementations.md | 121 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_Interface_Inheritance_Registers_Derived_Implementations.md diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs index ebc246f2099..179c51ec8c6 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/ClassBaseClassInspector.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using static System.StringComparison; using TypeInfo = HotChocolate.Types.Analyzers.Models.TypeInfo; namespace HotChocolate.Types.Analyzers.Inspectors; @@ -47,6 +48,14 @@ public bool TryHandle( return true; } + if (current.GetAttributes().Any( + t => t.AttributeClass?.ToDisplayString() + .StartsWith(WellKnownAttributes.InterfaceTypeAttribute, Ordinal) is true)) + { + syntaxInfo = new TypeInfo(typeDisplayString); + return true; + } + if (WellKnownTypes.TypeExtensionClass.Contains(displayString)) { syntaxInfo = new TypeExtensionInfo(typeDisplayString, false); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TypeModuleSyntaxGeneratorTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TypeModuleSyntaxGeneratorTests.cs index c4bcd8e7777..004e9754fa1 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/TypeModuleSyntaxGeneratorTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/TypeModuleSyntaxGeneratorTests.cs @@ -236,4 +236,40 @@ internal static partial class RootType """ ]).MatchMarkdownAsync(); } + + [Fact] + public async Task GenerateSource_Interface_Inheritance_Registers_Derived_Implementations() + { + await TestHelper.GetGeneratedSourceSnapshot( + [ + """ + using HotChocolate.Types; + + namespace TestNamespace; + + [InterfaceType] + public abstract class StatementTransaction + { + public int Id { get; set; } + } + + public sealed class DepositStatementTransaction : StatementTransaction + { + public decimal CollectionAmount { get; init; } + } + + public sealed class BillingStatementTransaction : StatementTransaction + { + public decimal FeeAndChargeAmount { get; init; } + } + + [QueryType] + public static partial class Query + { + public static StatementTransaction GetStatementTransaction() + => new DepositStatementTransaction { Id = 1, CollectionAmount = 42m }; + } + """ + ]).MatchMarkdownAsync(); + } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_Interface_Inheritance_Registers_Derived_Implementations.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_Interface_Inheritance_Registers_Derived_Implementations.md new file mode 100644 index 00000000000..43aed0e47d4 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/TypeModuleSyntaxGeneratorTests.GenerateSource_Interface_Inheritance_Registers_Derived_Implementations.md @@ -0,0 +1,121 @@ +# GenerateSource_Interface_Inheritance_Registers_Derived_Implementations + +## 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.AddType(); + builder.AddType(); + builder.ConfigureDescriptorContext(ctx => ctx.TypeConfiguration.TryAdd( + "Tests::TestNamespace.Query", + global::HotChocolate.Types.OperationTypeNames.Query, + () => global::TestNamespace.Query.Initialize)); + builder.AddType(); + builder.ConfigureSchema( + b => b.TryAddRootType( + () => new global::HotChocolate.Types.ObjectType( + d => d.Name(global::HotChocolate.Types.OperationTypeNames.Query)), + HotChocolate.Language.OperationType.Query)); + return builder; + } + } +} + +``` + +## Query.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 Query + { + internal static void Initialize(global::HotChocolate.Types.IObjectTypeDescriptor descriptor) + { + var extension = descriptor.Extend(); + var configuration = extension.Configuration; + var thisType = typeof(global::TestNamespace.Query); + var bindingResolver = extension.Context.ParameterBindingResolver; + var resolvers = new __Resolvers(); + + HotChocolate.Internal.ConfigurationHelper.ApplyConfiguration( + extension.Context, + descriptor, + null, + new global::HotChocolate.Types.QueryTypeAttribute()); + configuration.ConfigurationsAreApplied = true; + + var naming = descriptor.Extend().Context.Naming; + + descriptor + .Field(naming.GetMemberName("StatementTransaction", 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(global::TestNamespace.StatementTransaction), HotChocolate.Types.TypeContext.Output), + new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.NamedTypeNode("global__TestNamespace_StatementTransaction"))); + configuration.ResultType = typeof(global::TestNamespace.StatementTransaction); + + configuration.SetSourceGeneratorFlags(); + + configuration.Resolvers = context.Resolvers.GetStatementTransaction(); + }, + (Resolvers: resolvers, ThisType: thisType)); + + Configure(descriptor); + } + + static partial void Configure(global::HotChocolate.Types.IObjectTypeDescriptor descriptor); + + private sealed class __Resolvers + { + public HotChocolate.Resolvers.FieldResolverDelegates GetStatementTransaction() + { + return new global::HotChocolate.Resolvers.FieldResolverDelegates(pureResolver: GetStatementTransaction); + } + + private global::System.Object? GetStatementTransaction(global::HotChocolate.Resolvers.IResolverContext context) + { + var result = global::TestNamespace.Query.GetStatementTransaction(); + return result; + } + } + } +} + + +``` From 039e5d5c5d3d48aaaa36fd6258470a98436b21cb Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Feb 2026 23:09:54 +0000 Subject: [PATCH 2/2] Added test --- .../Data.PostgreSQL.Tests/IntegrationTests.cs | 32 +++++++++++++++++++ .../BillingStatementTransaction.cs | 6 ++++ .../DepositStatementTransaction.cs | 6 ++++ .../StatementTransaction.cs | 9 ++++++ .../StatementTransactionQueries.cs | 14 ++++++++ .../IntegrationTests.CreateSchema.graphql | 15 +++++++++ 6 files changed, 82 insertions(+) create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/BillingStatementTransaction.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/DepositStatementTransaction.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransaction.cs create mode 100644 src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransactionQueries.cs diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs index 468743f4f85..19dcf5bf324 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs @@ -454,6 +454,38 @@ public async Task Fallback_To_Runtime_Properties_When_No_Field_Is_Bindable() MatchSnapshot(result, interceptor); } + [Fact] + public async Task Query_InterfaceType_Derived_Implementation_Is_Resolved() + { + // act + var result = await ExecuteAsync( + """ + { + statementTransaction { + __typename + id + ... on DepositStatementTransaction { + collectionAmount + } + } + } + """); + + // assert + result.MatchInlineSnapshot( + """ + { + "data": { + "statementTransaction": { + "__typename": "DepositStatementTransaction", + "id": 1, + "collectionAmount": 42 + } + } + } + """); + } + [Fact] public async Task SecondLevelCache_Is_Used() { diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/BillingStatementTransaction.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/BillingStatementTransaction.cs new file mode 100644 index 00000000000..ef54f811128 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/BillingStatementTransaction.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Data.Types.StatementTransactions; + +public sealed class BillingStatementTransaction : StatementTransaction +{ + public int FeeAndChargeAmount { get; init; } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/DepositStatementTransaction.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/DepositStatementTransaction.cs new file mode 100644 index 00000000000..0d5106d54aa --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/DepositStatementTransaction.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Data.Types.StatementTransactions; + +public sealed class DepositStatementTransaction : StatementTransaction +{ + public int CollectionAmount { get; init; } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransaction.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransaction.cs new file mode 100644 index 00000000000..878924c70c4 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransaction.cs @@ -0,0 +1,9 @@ +using HotChocolate.Types; + +namespace HotChocolate.Data.Types.StatementTransactions; + +[InterfaceType] +public abstract class StatementTransaction +{ + public int Id { get; init; } +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransactionQueries.cs b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransactionQueries.cs new file mode 100644 index 00000000000..0c8e5123383 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/StatementTransactions/StatementTransactionQueries.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Data.Types.StatementTransactions; + +[QueryType] +public static partial class StatementTransactionQueries +{ + public static StatementTransaction GetStatementTransaction() + => new DepositStatementTransaction + { + Id = 1, + CollectionAmount = 42 + }; +} diff --git a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql index ea65b2c76c8..a6ec08f1e3c 100644 --- a/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql +++ b/src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql @@ -7,6 +7,15 @@ interface Node { id: ID! } +interface StatementTransaction { + id: Int! +} + +type BillingStatementTransaction implements StatementTransaction { + feeAndChargeAmount: Int! + id: Int! +} + type Brand implements Node { products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): BrandProductsConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") id: ID! @@ -71,6 +80,11 @@ type ConnectionPageInfo { endCursor: String } +type DepositStatementTransaction implements StatementTransaction { + collectionAmount: Int! + id: Int! +} + type ExpressionPerson { fullName: String @cost(weight: "10") id: Int! @@ -148,6 +162,7 @@ type Query { productById(id: ID!): Product @lookup @internal @cost(weight: "10") productsNonRelative("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductConnection! @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") singleProperties: [SingleProperty!]! @cost(weight: "10") + statementTransaction: StatementTransaction! } type SingleProperty {