Skip to content

Commit 2f0e206

Browse files
Add reproduction test
1 parent d05dc2f commit 2f0e206

6 files changed

Lines changed: 764 additions & 205 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace HotChocolate.Types;
4+
5+
public class ConnectionTests
6+
{
7+
[Fact]
8+
public async Task Build_Schema_Should_Fail_When_Same_Connection_Name_In_Two_Assemblies()
9+
{
10+
// arrange
11+
// Both assemblies source-generate a connection named "AuthorConnection" (and an
12+
// accompanying "AuthorEdge"). The second assembly references the first, but each
13+
// assembly bakes its own distinct CLR connection and edge ObjectType and registers
14+
// them through its own generated AddXxxTypes() extension method, so the runtime type
15+
// registry sees two different CLR types claiming the same GraphQL type name.
16+
const string assemblyOneSource =
17+
"""
18+
using System.Collections.Generic;
19+
using System.Threading;
20+
using System.Threading.Tasks;
21+
using HotChocolate;
22+
using HotChocolate.Types;
23+
using HotChocolate.Types.Pagination;
24+
25+
[assembly: Module("ConnectionAssemblyOneTypes")]
26+
27+
namespace Demo.CrossAssembly;
28+
29+
public sealed class Author
30+
{
31+
public int Id { get; set; }
32+
33+
public string Name { get; set; } = string.Empty;
34+
}
35+
36+
public class AuthorConnection : ConnectionBase<Author, AuthorEdge, ConnectionPageInfo>
37+
{
38+
public override IReadOnlyList<AuthorEdge> Edges => default!;
39+
40+
public IReadOnlyList<Author> Nodes => default!;
41+
42+
public override ConnectionPageInfo PageInfo => default!;
43+
44+
public int TotalCount => 0;
45+
}
46+
47+
public class AuthorEdge : IEdge<Author>
48+
{
49+
public Author Node => default!;
50+
51+
object? IEdge.Node => Node;
52+
53+
public string Cursor => default!;
54+
}
55+
56+
[QueryType]
57+
public static partial class ConnectionAssemblyOneQuery
58+
{
59+
public static Task<AuthorConnection> GetAuthors(
60+
GreenDonut.Data.PagingArguments pagingArgs,
61+
CancellationToken ct) => default!;
62+
}
63+
""";
64+
65+
const string assemblyTwoSource =
66+
"""
67+
using System.Collections.Generic;
68+
using System.Threading;
69+
using System.Threading.Tasks;
70+
using HotChocolate;
71+
using HotChocolate.Types;
72+
using HotChocolate.Types.Pagination;
73+
using Demo.CrossAssembly;
74+
75+
[assembly: Module("ConnectionAssemblyTwoTypes")]
76+
77+
namespace Demo.CrossAssembly.Two;
78+
79+
public class AuthorConnection : ConnectionBase<Author, AuthorEdge, ConnectionPageInfo>
80+
{
81+
public override IReadOnlyList<AuthorEdge> Edges => default!;
82+
83+
public IReadOnlyList<Author> Nodes => default!;
84+
85+
public override ConnectionPageInfo PageInfo => default!;
86+
87+
public int TotalCount => 0;
88+
}
89+
90+
public class AuthorEdge : IEdge<Author>
91+
{
92+
public Author Node => default!;
93+
94+
object? IEdge.Node => Node;
95+
96+
public string Cursor => default!;
97+
}
98+
99+
[QueryType]
100+
public static partial class ConnectionAssemblyTwoQuery
101+
{
102+
public static Task<AuthorConnection> GetMoreAuthors(
103+
GreenDonut.Data.PagingArguments pagingArgs,
104+
CancellationToken ct) => default!;
105+
}
106+
""";
107+
108+
var assemblies = new GeneratorAssembly[]
109+
{
110+
new("Demo.ConnectionAssemblyOne", assemblyOneSource),
111+
new(
112+
"Demo.ConnectionAssemblyTwo",
113+
[assemblyTwoSource],
114+
References: ["Demo.ConnectionAssemblyOne"])
115+
};
116+
117+
// act
118+
async Task BuildSchema()
119+
=> await GeneratorTestServer.CreateSchemaAsync(
120+
assemblies,
121+
configure: static b => b.AddPagingArguments(),
122+
disableDefaultSecurity: false);
123+
124+
var exception = await Assert.ThrowsAnyAsync<Exception>(BuildSchema);
125+
126+
// assert
127+
// Each assembly bakes its own distinct CLR companion for the connection and edge, so
128+
// the type registry sees two different types claiming the same GraphQL name and fails.
129+
// This documents the current cross-assembly limitation of the source generator.
130+
Assert.Contains("already registered", exception.ToString());
131+
}
132+
133+
[Fact]
134+
public async Task Build_Schema_Should_Reuse_Single_Companion_When_Same_Connection_Name_In_One_Assembly()
135+
{
136+
// arrange
137+
// A single compilation pages the same AuthorConnection from two different fields.
138+
// The generator runs once and de-duplicates the companion, so the runtime only ever
139+
// sees one "AuthorConnection" type and the schema builds without a collision.
140+
const string source =
141+
"""
142+
using System.Collections.Generic;
143+
using System.Threading;
144+
using System.Threading.Tasks;
145+
using HotChocolate;
146+
using HotChocolate.Types;
147+
using HotChocolate.Types.Pagination;
148+
149+
namespace Demo.SingleAssembly;
150+
151+
public sealed class Author
152+
{
153+
public int Id { get; set; }
154+
155+
public string Name { get; set; } = string.Empty;
156+
}
157+
158+
public class AuthorConnection : ConnectionBase<Author, AuthorEdge, ConnectionPageInfo>
159+
{
160+
public override IReadOnlyList<AuthorEdge> Edges => default!;
161+
162+
public IReadOnlyList<Author> Nodes => default!;
163+
164+
public override ConnectionPageInfo PageInfo => default!;
165+
166+
public int TotalCount => 0;
167+
}
168+
169+
public class AuthorEdge : IEdge<Author>
170+
{
171+
public Author Node => default!;
172+
173+
object? IEdge.Node => Node;
174+
175+
public string Cursor => default!;
176+
}
177+
178+
[QueryType]
179+
public static partial class Query
180+
{
181+
public static Task<AuthorConnection> GetAuthors(
182+
GreenDonut.Data.PagingArguments pagingArgs,
183+
CancellationToken ct) => default!;
184+
185+
public static Task<AuthorConnection> GetMoreAuthors(
186+
GreenDonut.Data.PagingArguments pagingArgs,
187+
CancellationToken ct) => default!;
188+
}
189+
""";
190+
191+
// act
192+
var schema = await GeneratorTestServer.CreateSchemaAsync(
193+
source,
194+
configure: static b => b.AddPagingArguments(),
195+
disableDefaultSecurity: false);
196+
197+
// assert
198+
// Both fields share the single generated AuthorConnection companion.
199+
var authors = schema.QueryType.Fields["authors"];
200+
var moreAuthors = schema.QueryType.Fields["moreAuthors"];
201+
Assert.Equal("AuthorConnection", authors.Type.NamedType().Name);
202+
Assert.Equal("AuthorConnection", moreAuthors.Type.NamedType().Name);
203+
Assert.Single(schema.Types, t => t.Name == "AuthorConnection");
204+
}
205+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Basic.Reference.Assemblies;
2+
using GreenDonut;
3+
using GreenDonut.Data;
4+
using HotChocolate.Data.Filters;
5+
using HotChocolate.Execution;
6+
using HotChocolate.Execution.Configuration;
7+
using HotChocolate.Execution.Processing;
8+
using HotChocolate.Features;
9+
using HotChocolate.Language;
10+
using HotChocolate.Language.Visitors;
11+
using HotChocolate.Types.Pagination;
12+
using Microsoft.AspNetCore.Builder;
13+
using Microsoft.CodeAnalysis;
14+
using Microsoft.Extensions.DependencyInjection;
15+
16+
namespace HotChocolate.Types;
17+
18+
/// <summary>
19+
/// Provides the metadata references that mirror the HotChocolate package graph an
20+
/// application using the source generator would compile against. The list is shared by
21+
/// every in-memory compilation in this test project so that the generated registration
22+
/// code, the HotChocolate runtime, and the test process all bind to the same loaded
23+
/// assemblies (a requirement for type identity to hold across assembly load contexts).
24+
/// </summary>
25+
internal static class GeneratorReferences
26+
{
27+
/// <summary>
28+
/// Gets the metadata references for an in-memory HotChocolate compilation. The list
29+
/// combines the framework reference assemblies for the active target framework with
30+
/// metadata references to the loaded HotChocolate assemblies.
31+
/// </summary>
32+
public static IReadOnlyList<PortableExecutableReference> All { get; } =
33+
[
34+
#if NET8_0
35+
.. Net80.References.All,
36+
#elif NET9_0
37+
.. Net90.References.All,
38+
#elif NET10_0
39+
.. Net100.References.All,
40+
#endif
41+
// HotChocolate.Primitives
42+
MetadataReference.CreateFromFile(typeof(ITypeSystemMember).Assembly.Location),
43+
44+
// HotChocolate.Execution
45+
MetadataReference.CreateFromFile(typeof(RequestDelegate).Assembly.Location),
46+
47+
// HotChocolate.Execution.Abstractions
48+
MetadataReference.CreateFromFile(typeof(RequestContext).Assembly.Location),
49+
50+
// HotChocolate.Execution.Processing
51+
MetadataReference.CreateFromFile(typeof(HotChocolateExecutionSelectionExtensions).Assembly.Location),
52+
53+
// HotChocolate.Execution.Abstractions
54+
MetadataReference.CreateFromFile(typeof(IRequestExecutorBuilder).Assembly.Location),
55+
56+
// HotChocolate.Execution.Operation.Abstractions
57+
MetadataReference.CreateFromFile(typeof(ISelection).Assembly.Location),
58+
59+
// HotChocolate.Types
60+
MetadataReference.CreateFromFile(typeof(ObjectTypeAttribute).Assembly.Location),
61+
MetadataReference.CreateFromFile(typeof(QueryTypeAttribute).Assembly.Location),
62+
MetadataReference.CreateFromFile(typeof(Connection).Assembly.Location),
63+
MetadataReference.CreateFromFile(typeof(PageConnection<>).Assembly.Location),
64+
65+
// HotChocolate.Types.Abstractions
66+
MetadataReference.CreateFromFile(typeof(ISchemaDefinition).Assembly.Location),
67+
68+
// HotChocolate.Features
69+
MetadataReference.CreateFromFile(typeof(IFeatureProvider).Assembly.Location),
70+
71+
// HotChocolate.Language
72+
MetadataReference.CreateFromFile(typeof(OperationType).Assembly.Location),
73+
74+
// HotChocolate.Language.Utf8
75+
MetadataReference.CreateFromFile(typeof(ParserOptions).Assembly.Location),
76+
77+
// HotChocolate.Language.Visitors
78+
MetadataReference.CreateFromFile(typeof(SyntaxVisitor).Assembly.Location),
79+
80+
// HotChocolate.Abstractions
81+
MetadataReference.CreateFromFile(typeof(ParentAttribute).Assembly.Location),
82+
83+
// HotChocolate.AspNetCore
84+
MetadataReference.CreateFromFile(
85+
typeof(HotChocolateAspNetCoreServiceCollectionExtensions).Assembly.Location),
86+
87+
// GreenDonut
88+
MetadataReference.CreateFromFile(typeof(DataLoaderBase<,>).Assembly.Location),
89+
MetadataReference.CreateFromFile(typeof(IDataLoader).Assembly.Location),
90+
91+
// GreenDonut.Data
92+
MetadataReference.CreateFromFile(typeof(PagingArguments).Assembly.Location),
93+
MetadataReference.CreateFromFile(typeof(IPredicateBuilder).Assembly.Location),
94+
MetadataReference.CreateFromFile(typeof(DefaultPredicateBuilder).Assembly.Location),
95+
96+
// HotChocolate.Data
97+
MetadataReference.CreateFromFile(typeof(IFilterContext).Assembly.Location),
98+
99+
// Microsoft.AspNetCore
100+
MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location),
101+
102+
// Microsoft.Extensions.DependencyInjection.Abstractions
103+
MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location),
104+
105+
// Microsoft.AspNetCore.Authorization
106+
MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute).Assembly.Location),
107+
108+
// HotChocolate.Authorization
109+
MetadataReference.CreateFromFile(typeof(Authorization.AuthorizeAttribute).Assembly.Location),
110+
111+
// HotChocolate.Types.OffsetPagination
112+
MetadataReference.CreateFromFile(typeof(UseOffsetPagingAttribute).Assembly.Location)
113+
];
114+
}

0 commit comments

Comments
 (0)