From a2bcad2789b889e42433e44662fa768b2a1bfab4 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 20 May 2026 11:35:57 +0200 Subject: [PATCH] Fix FactoryTypeReference cache collision for shared CLR types --- .../TypeReferences/FactoryTypeReference.cs | 7 ++++++- .../{Product.cs => Types.cs} | 18 ++++++++++++++++++ .../IntegrationTests.Schema_Snapshot.snap | 16 ++++++++++++++++ ...pshot_Without_ConnectionName_Inference.snap | 16 ++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) rename src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/{Product.cs => Types.cs} (90%) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/TypeReferences/FactoryTypeReference.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/TypeReferences/FactoryTypeReference.cs index ce8d2d3fb78..363f4dd6290 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/TypeReferences/FactoryTypeReference.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/TypeReferences/FactoryTypeReference.cs @@ -44,7 +44,12 @@ public string Key { get { - field ??= TypeStructure.ToString(indented: false); + if (field is null) + { + var structure = TypeStructure.ToString(indented: false); + field = Context == TypeContext.None ? structure : $"{structure}_{Context}"; + } + return field; } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Product.cs b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Types.cs similarity index 90% rename from src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Product.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Types.cs index 9db30d1a1a5..159a368dff3 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Product.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/Types.cs @@ -109,6 +109,24 @@ public static string ListNullableElementArgumentRef(List items) public static string NullableListNullableElementArgumentRef(List? items) => throw new Exception(); + + // Covers a regression where the same POCO used in both an output position + // (resolver return) and an input position (mutation argument) emitted two + // distinct GraphQL types but the FactoryTypeReference cache keyed only on + // the syntactic structure, so one position reused the IType of the other. + public static IReadOnlyCollection GetShapes() => []; +} + +public class Shape +{ + public required string Key { get; set; } + public required string Name { get; set; } +} + +[MutationType] +public static partial class Mutation +{ + public static bool SaveShapes(IReadOnlyList shapes) => true; } public class IsSelectedNode 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 896faa449a7..7e110ead6a0 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 + mutation: Mutation subscription: Subscription } @@ -92,9 +93,14 @@ type Query { listArgumentRef(items: [String!]!): String! listNullableElementArgumentRef(items: [String]!): String! nullableListNullableElementArgumentRef(items: [String]): String! + shapes: [Shape!]! issue8057Entity(id: ID!): Issue8057Entity @cost(weight: "10") } +type Mutation { + saveShapes(shapes: [ShapeInput!]!): Boolean! +} + type Subscription { onProductAdded(categoryId: Int!): Int! @cost(weight: "10") onProductPriceChanged(productId: Int!): Int! @cost(weight: "10") @@ -201,6 +207,11 @@ type ProductsEdge { node: Product! } +type Shape { + key: String! + name: String! +} + type Television implements Product { argumentWithExplicitType(arg: Version!): String! nullableArgumentWithExplicitType(arg: Version): String! @@ -218,6 +229,11 @@ interface Product { id: String! } +input ShapeInput { + key: String! + name: String! +} + scalar Version scalar Version2 diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot_Without_ConnectionName_Inference.snap b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot_Without_ConnectionName_Inference.snap index d68ef66c10d..0deabeb0c22 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot_Without_ConnectionName_Inference.snap +++ b/src/HotChocolate/Core/test/Types.Analyzers.Integration.Tests/__snapshots__/IntegrationTests.Schema_Snapshot_Without_ConnectionName_Inference.snap @@ -1,5 +1,6 @@ schema { query: Query + mutation: Mutation subscription: Subscription } @@ -67,9 +68,14 @@ type Query { listArgumentRef(items: [String!]!): String! listNullableElementArgumentRef(items: [String]!): String! nullableListNullableElementArgumentRef(items: [String]): String! + shapes: [Shape!]! issue8057Entity(id: ID!): Issue8057Entity } +type Mutation { + saveShapes(shapes: [ShapeInput!]!): Boolean! +} + type Subscription { onProductAdded(categoryId: Int!): Int! onProductPriceChanged(productId: Int!): Int! @@ -158,6 +164,11 @@ type ProductEdge { node: Product! } +type Shape { + key: String! + name: String! +} + type Television implements Product { argumentWithExplicitType(arg: Version!): String! nullableArgumentWithExplicitType(arg: Version): String! @@ -175,6 +186,11 @@ interface Product { id: String! } +input ShapeInput { + key: String! + name: String! +} + scalar Version scalar Version2