diff --git a/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.Designer.cs b/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.Designer.cs
index e784e19f1df..dd211007a8b 100644
--- a/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.Designer.cs
+++ b/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.Designer.cs
@@ -68,6 +68,33 @@ internal static string NonNullType_InnerTypeCannotBeNonNull {
}
}
+ ///
+ /// Looks up a localized string similar to The argument '{0}' on field '{1}' was extended with a different default value than the original definition..
+ ///
+ internal static string SchemaParser_ArgumentDefaultValueMismatchInExtension {
+ get {
+ return ResourceManager.GetString("SchemaParser_ArgumentDefaultValueMismatchInExtension", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The description on argument '{0}' of field '{1}' in the extension does not match the original definition..
+ ///
+ internal static string SchemaParser_ArgumentDescriptionMismatchInExtension {
+ get {
+ return ResourceManager.GetString("SchemaParser_ArgumentDescriptionMismatchInExtension", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The argument '{0}' on field '{1}' was extended with a different type than the original definition..
+ ///
+ internal static string SchemaParser_ArgumentTypeMismatchInExtension {
+ get {
+ return ResourceManager.GetString("SchemaParser_ArgumentTypeMismatchInExtension", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to An argument with the name '{0}' has already been defined on the field '{1}'..
///
@@ -113,6 +140,24 @@ internal static string SchemaParser_DuplicateTypeDefinition {
}
}
+ ///
+ /// Looks up a localized string similar to The description on field '{0}' of type '{1}' in the extension does not match the original definition..
+ ///
+ internal static string SchemaParser_FieldDescriptionMismatchInExtension {
+ get {
+ return ResourceManager.GetString("SchemaParser_FieldDescriptionMismatchInExtension", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The field '{0}' on type '{1}' was extended with a different type than the original definition..
+ ///
+ internal static string SchemaParser_FieldTypeMismatchInExtension {
+ get {
+ return ResourceManager.GetString("SchemaParser_FieldTypeMismatchInExtension", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The argument '{0}' must accept an input type..
///
@@ -148,5 +193,14 @@ internal static string SchemaParser_InvalidUnionMemberType {
return ResourceManager.GetString("SchemaParser_InvalidUnionMemberType", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to The non-repeatable directive '@{0}' was already applied to '{1}' and cannot be applied again by an extension..
+ ///
+ internal static string SchemaParser_NonRepeatableDirectiveAlreadyApplied {
+ get {
+ return ResourceManager.GetString("SchemaParser_NonRepeatableDirectiveAlreadyApplied", resourceCulture);
+ }
+ }
}
}
diff --git a/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.resx b/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.resx
index 0f3fad8a039..a5f0adca084 100644
--- a/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.resx
+++ b/src/HotChocolate/Mutable/src/Types.Mutable/Properties/MutableResources.resx
@@ -21,6 +21,15 @@
The inner type cannot be a non-null type.
+
+ The argument '{0}' on field '{1}' was extended with a different default value than the original definition.
+
+
+ The description on argument '{0}' of field '{1}' in the extension does not match the original definition.
+
+
+ The argument '{0}' on field '{1}' was extended with a different type than the original definition.
+
An argument with the name '{0}' has already been defined on the field '{1}'.
@@ -36,6 +45,12 @@
A type with the name '{0}' has already been defined.
+
+ The description on field '{0}' of type '{1}' in the extension does not match the original definition.
+
+
+ The field '{0}' on type '{1}' was extended with a different type than the original definition.
+
The argument '{0}' must accept an input type.
@@ -48,4 +63,7 @@
The Union type '{0}' cannot include the type '{1}'. Unions can only contain Object types.
+
+ The non-repeatable directive '@{0}' was already applied to '{1}' and cannot be applied again by an extension.
+
diff --git a/src/HotChocolate/Mutable/src/Types.Mutable/Serialization/SchemaParser.cs b/src/HotChocolate/Mutable/src/Types.Mutable/Serialization/SchemaParser.cs
index 0a9eaed3b10..dab54002b4e 100644
--- a/src/HotChocolate/Mutable/src/Types.Mutable/Serialization/SchemaParser.cs
+++ b/src/HotChocolate/Mutable/src/Types.Mutable/Serialization/SchemaParser.cs
@@ -53,8 +53,8 @@ public static void Parse(
DiscoverExtensions(schema, document);
BuildTypes(schema, document, skippedNodes);
- ExtendTypes(schema, document, skippedNodes);
BuildDirectiveTypes(schema, document, skippedNodes);
+ ExtendTypes(schema, document, skippedNodes);
BuildAndExtendSchema(schema, document, skippedNodes);
}
@@ -413,6 +413,9 @@ private static void BuildComplexType(
MutableComplexTypeDefinition type,
ComplexTypeDefinitionNodeBase node)
{
+ var isExtension = node is ObjectTypeExtensionNode or InterfaceTypeExtensionNode;
+ var seenFieldNames = new HashSet(StringComparer.Ordinal);
+
BuildDirectiveCollection(schema, type.Directives, node.Directives);
foreach (var interfaceRef in node.Interfaces)
@@ -422,7 +425,7 @@ private static void BuildComplexType(
foreach (var fieldNode in node.Fields)
{
- if (type.Fields.ContainsName(fieldNode.Name.Value))
+ if (!seenFieldNames.Add(fieldNode.Name.Value))
{
throw new SchemaInitializationException(
string.Format(
@@ -431,6 +434,21 @@ private static void BuildComplexType(
type.Name));
}
+ if (type.Fields.TryGetField(fieldNode.Name.Value, out var existingField))
+ {
+ if (!isExtension)
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_DuplicateFieldDefinition,
+ fieldNode.Name.Value,
+ type.Name));
+ }
+
+ ExtendOutputField(schema, type, existingField, fieldNode);
+ continue;
+ }
+
var builtFieldType = schema.Types.BuildType(fieldNode.Type);
if (builtFieldType is not IOutputType fieldType)
@@ -467,36 +485,179 @@ private static void BuildComplexType(
$"{type.Name}.{field.Name}"));
}
- var builtArgumentType = schema.Types.BuildType(argumentNode.Type);
+ field.Arguments.Add(BuildArgument(schema, field, argumentNode));
+ }
- if (builtArgumentType is not IInputType argumentType)
- {
- throw new SchemaInitializationException(
- string.Format(
- SchemaParser_InvalidArgumentType,
- $"{type.Name}.{field.Name}({argumentNode.Name.Value}:)"));
- }
+ type.Fields.Add(field);
+ }
+ }
- var argument = new MutableInputFieldDefinition(argumentNode.Name.Value)
- {
- Description = argumentNode.Description?.Value,
- Type = argumentType,
- DefaultValue = argumentNode.DefaultValue,
- DeclaringMember = field
- };
+ private static MutableInputFieldDefinition BuildArgument(
+ MutableSchemaDefinition schema,
+ MutableOutputFieldDefinition field,
+ InputValueDefinitionNode argumentNode)
+ {
+ var builtArgumentType = schema.Types.BuildType(argumentNode.Type);
- BuildDirectiveCollection(schema, argument.Directives, argumentNode.Directives);
+ if (builtArgumentType is not IInputType argumentType)
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_InvalidArgumentType,
+ $"{field.DeclaringMember?.Name}.{field.Name}({argumentNode.Name.Value}:)"));
+ }
- if (IsDeprecated(argument.Directives, out reason))
- {
- argument.IsDeprecated = true;
- argument.DeprecationReason = reason;
- }
+ var argument = new MutableInputFieldDefinition(argumentNode.Name.Value)
+ {
+ Description = argumentNode.Description?.Value,
+ Type = argumentType,
+ DefaultValue = argumentNode.DefaultValue,
+ DeclaringMember = field
+ };
+
+ BuildDirectiveCollection(schema, argument.Directives, argumentNode.Directives);
+
+ if (IsDeprecated(argument.Directives, out var reason))
+ {
+ argument.IsDeprecated = true;
+ argument.DeprecationReason = reason;
+ }
- field.Arguments.Add(argument);
+ return argument;
+ }
+
+ private static void ExtendOutputField(
+ MutableSchemaDefinition schema,
+ MutableComplexTypeDefinition type,
+ MutableOutputFieldDefinition existingField,
+ FieldDefinitionNode fieldNode)
+ {
+ var builtFieldType = schema.Types.BuildType(fieldNode.Type);
+
+ if (builtFieldType is not IOutputType fieldType)
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_InvalidFieldType,
+ $"{type.Name}.{fieldNode.Name.Value}"));
+ }
+
+ if (!existingField.Type.Equals(fieldType, TypeComparison.Structural))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_FieldTypeMismatchInExtension,
+ fieldNode.Name.Value,
+ type.Name));
+ }
+
+ if (fieldNode.Description is not null
+ && !string.Equals(
+ fieldNode.Description.Value,
+ existingField.Description,
+ StringComparison.Ordinal))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_FieldDescriptionMismatchInExtension,
+ fieldNode.Name.Value,
+ type.Name));
+ }
+
+ MergeDirectives(
+ schema,
+ existingField.Directives,
+ fieldNode.Directives,
+ $"{type.Name}.{existingField.Name}");
+
+ if (IsDeprecated(existingField.Directives, out var reason))
+ {
+ existingField.IsDeprecated = true;
+ existingField.DeprecationReason = reason;
+ }
+
+ var seenArgumentNames = new HashSet(StringComparer.Ordinal);
+
+ foreach (var argumentNode in fieldNode.Arguments)
+ {
+ if (!seenArgumentNames.Add(argumentNode.Name.Value))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_DuplicateArgumentDefinition,
+ argumentNode.Name.Value,
+ $"{type.Name}.{existingField.Name}"));
}
- type.Fields.Add(field);
+ if (existingField.Arguments.TryGetField(argumentNode.Name.Value, out var existingArg))
+ {
+ ExtendArgument(schema, existingField, existingArg, argumentNode);
+ }
+ else
+ {
+ existingField.Arguments.Add(BuildArgument(schema, existingField, argumentNode));
+ }
+ }
+ }
+
+ private static void ExtendArgument(
+ MutableSchemaDefinition schema,
+ MutableOutputFieldDefinition field,
+ MutableInputFieldDefinition existingArg,
+ InputValueDefinitionNode argumentNode)
+ {
+ var builtArgumentType = schema.Types.BuildType(argumentNode.Type);
+
+ if (builtArgumentType is not IInputType argumentType)
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_InvalidArgumentType,
+ $"{field.DeclaringMember?.Name}.{field.Name}({argumentNode.Name.Value}:)"));
+ }
+
+ if (!existingArg.Type.Equals(argumentType, TypeComparison.Structural))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_ArgumentTypeMismatchInExtension,
+ argumentNode.Name.Value,
+ $"{field.DeclaringMember?.Name}.{field.Name}"));
+ }
+
+ if (argumentNode.DefaultValue is not null
+ && !SyntaxComparer.BySyntax.Equals(existingArg.DefaultValue, argumentNode.DefaultValue))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_ArgumentDefaultValueMismatchInExtension,
+ argumentNode.Name.Value,
+ $"{field.DeclaringMember?.Name}.{field.Name}"));
+ }
+
+ if (argumentNode.Description is not null
+ && !string.Equals(
+ argumentNode.Description.Value,
+ existingArg.Description,
+ StringComparison.Ordinal))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_ArgumentDescriptionMismatchInExtension,
+ argumentNode.Name.Value,
+ $"{field.DeclaringMember?.Name}.{field.Name}"));
+ }
+
+ MergeDirectives(
+ schema,
+ existingArg.Directives,
+ argumentNode.Directives,
+ $"{field.DeclaringMember?.Name}.{field.Name}({existingArg.Name}:)");
+
+ if (IsDeprecated(existingArg.Directives, out var reason))
+ {
+ existingArg.IsDeprecated = true;
+ existingArg.DeprecationReason = reason;
}
}
@@ -809,45 +970,81 @@ private static void BuildDirectiveCollection(
{
foreach (var directiveNode in nodes)
{
- if (!schema.DirectiveDefinitions.TryGetDirective(
- directiveNode.Name.Value,
- out var directiveType))
- {
- switch (directiveNode.Name.Value)
- {
- case DirectiveNames.Deprecated.Name:
- directiveType = BuiltIns.Deprecated.Create(schema);
- break;
-
- case DirectiveNames.OneOf.Name:
- directiveType = BuiltIns.OneOf.Create();
- break;
-
- case DirectiveNames.SpecifiedBy.Name:
- directiveType = BuiltIns.SpecifiedBy.Create(schema);
- break;
+ var directiveType = ResolveDirectiveDefinition(schema, directiveNode);
- default:
- directiveType = new MutableDirectiveDefinition(directiveNode.Name.Value)
- {
- IsRepeatable = true,
- Locations = DirectiveLocation.TypeSystem
- };
-
- directiveType.Features.Set(
- new IncompleteDirectiveDefinitionFeature { IsIncomplete = true });
+ var directive = new Directive(
+ directiveType,
+ directiveNode.Arguments.Select(t => new ArgumentAssignment(t.Name.Value, t.Value)).ToList());
+ directives.Add(directive);
+ }
+ }
- break;
- }
+ private static void MergeDirectives(
+ MutableSchemaDefinition schema,
+ DirectiveCollection target,
+ IReadOnlyList nodes,
+ string targetName)
+ {
+ foreach (var directiveNode in nodes)
+ {
+ var directiveType = ResolveDirectiveDefinition(schema, directiveNode);
- schema.DirectiveDefinitions.Add(directiveType);
+ if (!directiveType.IsRepeatable && target.ContainsName(directiveType.Name))
+ {
+ throw new SchemaInitializationException(
+ string.Format(
+ SchemaParser_NonRepeatableDirectiveAlreadyApplied,
+ directiveType.Name,
+ targetName));
}
var directive = new Directive(
directiveType,
directiveNode.Arguments.Select(t => new ArgumentAssignment(t.Name.Value, t.Value)).ToList());
- directives.Add(directive);
+ target.Add(directive);
+ }
+ }
+
+ private static MutableDirectiveDefinition ResolveDirectiveDefinition(
+ MutableSchemaDefinition schema,
+ DirectiveNode directiveNode)
+ {
+ if (schema.DirectiveDefinitions.TryGetDirective(
+ directiveNode.Name.Value,
+ out var directiveType))
+ {
+ return directiveType;
}
+
+ switch (directiveNode.Name.Value)
+ {
+ case DirectiveNames.Deprecated.Name:
+ directiveType = BuiltIns.Deprecated.Create(schema);
+ break;
+
+ case DirectiveNames.OneOf.Name:
+ directiveType = BuiltIns.OneOf.Create();
+ break;
+
+ case DirectiveNames.SpecifiedBy.Name:
+ directiveType = BuiltIns.SpecifiedBy.Create(schema);
+ break;
+
+ default:
+ directiveType = new MutableDirectiveDefinition(directiveNode.Name.Value)
+ {
+ IsRepeatable = true,
+ Locations = DirectiveLocation.TypeSystem
+ };
+
+ directiveType.Features.Set(
+ new IncompleteDirectiveDefinitionFeature { IsIncomplete = true });
+
+ break;
+ }
+
+ schema.DirectiveDefinitions.Add(directiveType);
+ return directiveType;
}
private static bool IsDeprecated(DirectiveCollection directives, out string? reason)
diff --git a/src/HotChocolate/Mutable/test/Types.Mutable.Tests/SchemaParserTests.cs b/src/HotChocolate/Mutable/test/Types.Mutable.Tests/SchemaParserTests.cs
index eccbd04f38d..bb20d78ea33 100644
--- a/src/HotChocolate/Mutable/test/Types.Mutable.Tests/SchemaParserTests.cs
+++ b/src/HotChocolate/Mutable/test/Types.Mutable.Tests/SchemaParserTests.cs
@@ -465,4 +465,324 @@ directive @foo on FIELD
"A directive with the name '@foo' has already been defined.",
Assert.Throws(Action).Message);
}
+
+ [Fact]
+ public void Parse_Should_AddDirectiveToExistingField_When_ExtensionRedeclaresField()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query { id: String }
+
+ extend type Query {
+ id: String @deprecated(reason: "Use newId")
+ }
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var queryType = Assert.IsType(schema.Types["Query"]);
+ var field = Assert.Single(queryType.Fields.AsEnumerable());
+ Assert.Equal("id", field.Name);
+ Assert.True(field.IsDeprecated);
+ Assert.Equal("Use newId", field.DeprecationReason);
+ Assert.True(field.Directives.ContainsName("deprecated"));
+ }
+
+ [Fact]
+ public void Parse_Should_AddArgumentToExistingField_When_ExtensionAddsNewArgument()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query {
+ user(id: String): String
+ }
+
+ extend type Query {
+ user(name: String): String
+ }
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var queryType = Assert.IsType(schema.Types["Query"]);
+ var field = Assert.Single(queryType.Fields.AsEnumerable());
+ Assert.Collection(
+ field.Arguments.AsEnumerable(),
+ arg => Assert.Equal("id", arg.Name),
+ arg => Assert.Equal("name", arg.Name));
+ }
+
+ [Fact]
+ public void Parse_Should_AddDirectiveToExistingArgument_When_ExtensionRedeclaresArgument()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query {
+ user(id: String): String
+ }
+
+ extend type Query {
+ user(id: String @deprecated(reason: "Use newId")): String
+ }
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var queryType = Assert.IsType(schema.Types["Query"]);
+ var field = Assert.Single(queryType.Fields.AsEnumerable());
+ var argument = Assert.Single(field.Arguments.AsEnumerable());
+ Assert.Equal("id", argument.Name);
+ Assert.True(argument.IsDeprecated);
+ Assert.Equal("Use newId", argument.DeprecationReason);
+ Assert.True(argument.Directives.ContainsName("deprecated"));
+ }
+
+ [Fact]
+ public void Parse_Should_RetainOriginalDescription_When_ExtensionRedeclaresFieldWithoutDescription()
+ {
+ // arrange
+ const string sdl =
+ """"
+ type Query {
+ """desc"""
+ id: String
+ }
+
+ extend type Query {
+ id: String @deprecated
+ }
+ """";
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var queryType = Assert.IsType(schema.Types["Query"]);
+ var field = Assert.Single(queryType.Fields.AsEnumerable());
+ Assert.Equal("desc", field.Description);
+ Assert.True(field.IsDeprecated);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionRedeclaresFieldWithDifferentDescription()
+ {
+ // arrange
+ const string sdl =
+ """"
+ type Query {
+ """original"""
+ id: String
+ }
+
+ extend type Query {
+ """different"""
+ id: String
+ }
+ """";
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "The description on field 'id' of type 'Query' in the extension does not match the original definition.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionFieldTypeMismatches()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query { id: String }
+
+ extend type Query {
+ id: Int
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "The field 'id' on type 'Query' was extended with a different type than the original definition.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionArgumentDefaultValueMismatches()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query {
+ user(id: String = "a"): String
+ }
+
+ extend type Query {
+ user(id: String = "b"): String
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "The argument 'id' on field 'Query.user' was extended with a different default value than the original "
+ + "definition.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionAppliesNonRepeatableDirectiveAlreadyApplied()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query {
+ id: String @deprecated(reason: "first")
+ }
+
+ extend type Query {
+ id: String @deprecated(reason: "second")
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "The non-repeatable directive '@deprecated' was already applied to 'Query.id' "
+ + "and cannot be applied again by an extension.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_AppendBothDirectives_When_ExtensionAppliesRepeatableDirectiveAlreadyApplied()
+ {
+ // arrange
+ const string sdl =
+ """
+ directive @tag(name: String!) repeatable on FIELD_DEFINITION
+
+ type Query {
+ id: String @tag(name: "a")
+ }
+
+ extend type Query {
+ id: String @tag(name: "b")
+ }
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var queryType = Assert.IsType(schema.Types["Query"]);
+ var field = Assert.Single(queryType.Fields.AsEnumerable());
+ Assert.Equal(2, field.Directives["tag"].Count());
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionContainsDuplicateFieldNamesWithinItself()
+ {
+ // arrange
+ const string sdl =
+ """
+ extend type Query {
+ id: String
+ id: String
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "A field with the name 'id' has already been defined on the type 'Query'.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionFieldDeclaresDuplicateArgumentName()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query { user: String }
+
+ extend type Query {
+ user(id: String, id: String): String
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "An argument with the name 'id' has already been defined on the field 'Query.user'.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_Throw_When_ExtensionRedeclaresArgumentTwiceWithinSameFieldExtension()
+ {
+ // arrange
+ const string sdl =
+ """
+ type Query { user(id: String): String }
+
+ extend type Query {
+ user(id: String, id: String): String
+ }
+ """;
+
+ // act
+ static void Action() => SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ Assert.Equal(
+ "An argument with the name 'id' has already been defined on the field 'Query.user'.",
+ Assert.Throws(Action).Message);
+ }
+
+ [Fact]
+ public void Parse_Should_AddDirectiveToExistingField_When_InterfaceExtensionRedeclaresField()
+ {
+ // arrange
+ const string sdl =
+ """
+ interface Node { id: ID }
+
+ extend interface Node {
+ id: ID @deprecated(reason: "Use newId")
+ }
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+
+ // assert
+ var nodeType = Assert.IsType(schema.Types["Node"]);
+ var field = Assert.Single(nodeType.Fields.AsEnumerable());
+ Assert.Equal("id", field.Name);
+ Assert.True(field.IsDeprecated);
+ Assert.Equal("Use newId", field.DeprecationReason);
+ Assert.True(field.Directives.ContainsName("deprecated"));
+ }
}