diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs index 1c25aeeb80f..3080c8dc31d 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionMiddleware.cs @@ -27,10 +27,10 @@ public async ValueTask InvokeAsync(IMiddlewareContext context) foreach (var argument in _resolverArguments) { - input.TryGetValue(argument.Name, out var value); + input.TryGetValue(argument.InputName, out var value); var omitted = false; - if (!inputLiteral.TryGetValue(argument.Name, out var valueLiteral)) + if (!inputLiteral.TryGetValue(argument.InputName, out var valueLiteral)) { omitted = true; valueLiteral = argument.DefaultValue; diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs index efb1bd28732..a4411e6d29d 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs @@ -214,13 +214,15 @@ private void TryApplyInputConvention( return; } - var inputType = CreateInputType(inputTypeName, mutation); + var inputFieldNameMap = CreateInputFieldNameMap(mutation); + var inputType = CreateInputType(inputTypeName, mutation, inputFieldNameMap); RegisterType(inputType); var resolverArguments = new List(); foreach (var argument in mutation.Arguments) { + var inputFieldName = inputFieldNameMap[argument.Name]; var runtimeType = argument.RuntimeType ?? argument.Parameter?.ParameterType ?? typeof(object); @@ -249,7 +251,8 @@ private void TryApplyInputConvention( resolverArguments.Add( new ResolverArgument( argument.Name, - new SchemaCoordinate(inputTypeName, memberName: argument.Name), + inputFieldName, + new SchemaCoordinate(inputTypeName, memberName: inputFieldName), _completionContext.GetType(argument.Type!), runtimeType, defaultValue, @@ -448,7 +451,9 @@ private void TryApplyPayloadConvention( // lastly we will create the mutation payload and replace with it the current mutation // result type. - payloadFieldName ??= _context.Naming.FormatFieldName(registration.Type.Name); + payloadFieldName ??= _context.Naming.GetMemberName( + registration.Type.Name, + MemberKind.ObjectField); var type = CreatePayloadType( payloadTypeName, @@ -463,9 +468,25 @@ private void TryApplyPayloadConvention( mutation.Features.Set(null); } + private Dictionary CreateInputFieldNameMap( + ObjectFieldConfiguration fieldDef) + { + var inputFieldNameMap = new Dictionary(StringComparer.Ordinal); + + foreach (var argumentDef in fieldDef.Arguments) + { + inputFieldNameMap.Add( + argumentDef.Name, + _context.Naming.GetMemberName(argumentDef.Name, MemberKind.InputObjectField)); + } + + return inputFieldNameMap; + } + private static InputObjectType CreateInputType( string typeName, - ObjectFieldConfiguration fieldDef) + ObjectFieldConfiguration fieldDef, + IReadOnlyDictionary inputFieldNameMap) { var inputObjectDef = new InputObjectTypeConfiguration(typeName); @@ -473,6 +494,7 @@ private static InputObjectType CreateInputType( { var inputFieldDef = new InputFieldConfiguration(); argumentDef.CopyTo(inputFieldDef); + inputFieldDef.Name = inputFieldNameMap[argumentDef.Name]; inputFieldDef.RuntimeType = argumentDef.RuntimeType ?? @@ -744,17 +766,47 @@ private readonly ref struct Options( public string FormatInputTypeName(string mutationName) => InputTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - char.ToUpper(mutationName[0]) + mutationName[1..]); + FormatMutationName(mutationName)); public string FormatPayloadTypeName(string mutationName) => PayloadTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - char.ToUpper(mutationName[0]) + mutationName[1..]); + FormatMutationName(mutationName)); public string FormatErrorTypeName(string mutationName) => PayloadErrorTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - char.ToUpper(mutationName[0]) + mutationName[1..]); + FormatMutationName(mutationName)); + + private static string FormatMutationName(string mutationName) + { + if (string.IsNullOrEmpty(mutationName)) + { + return mutationName; + } + + if (mutationName.IndexOf('_', StringComparison.Ordinal) < 0) + { + return char.ToUpperInvariant(mutationName[0]) + mutationName[1..]; + } + + var builder = new System.Text.StringBuilder(mutationName.Length); + var upperNext = true; + + foreach (var c in mutationName) + { + if (c == '_') + { + upperNext = true; + continue; + } + + builder.Append(upperNext ? char.ToUpperInvariant(c) : c); + upperNext = false; + } + + return builder.ToString(); + } } private readonly struct FieldDef(string name, TypeReference type) diff --git a/src/HotChocolate/Core/src/Types.Mutations/ResolverArgument.cs b/src/HotChocolate/Core/src/Types.Mutations/ResolverArgument.cs index 401ad1f5229..339485d0f7f 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/ResolverArgument.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/ResolverArgument.cs @@ -2,6 +2,7 @@ namespace HotChocolate.Types; internal sealed class ResolverArgument( string name, + string inputName, SchemaCoordinate coordinate, IInputType type, Type runtimeType, @@ -11,6 +12,8 @@ internal sealed class ResolverArgument( { public string Name { get; } = name; + public string InputName { get; } = inputName; + public SchemaCoordinate Coordinate { get; } = coordinate; public IInputType Type { get; } = type; diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs index 651368a1cde..7487b9788dd 100644 --- a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs @@ -1266,11 +1266,41 @@ public async Task Mutation_With_MutationConventionsAndNamingConventions() "name_Named": "coco" } } - } + } } """); } + [Fact] + public async Task MutationConvention_With_SnakeCase_ObjectField_NamingConvention_Uses_PascalCase_TypeNames() + { + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddMutationType() + .AddConvention() + .AddMutationConventions( + new MutationConventionOptions { ApplyToAllMutations = true }) + .ModifyOptions(o => o.StrictValidation = false) + .BuildSchemaAsync(); + + schema.MatchSnapshot(); + + var schemaText = schema.ToString(); + + Assert.Matches( + @"type Issue4803Mutation \{[\s\S]*do_something\(input: DoSomethingInput!\): DoSomethingPayload!", + schemaText); + Assert.Contains("input DoSomethingInput {", schemaText); + Assert.Contains("type DoSomethingPayload {", schemaText); + Assert.Contains("issue4803_result: Issue4803Result", schemaText); + Assert.Contains("user_name: String!", schemaText); + Assert.DoesNotContain("Do_somethingInput", schemaText); + Assert.DoesNotContain("Do_somethingPayload", schemaText); + Assert.DoesNotContain("issue4803Result: Issue4803Result", schemaText); + Assert.DoesNotContain("userName: String!", schemaText); + } + [Fact] public async Task Mutation_With_ErrorAnnotatedAndCustomInterface_LateAndEarlyRegistration() { @@ -1933,4 +1963,45 @@ public override string GetTypeDescription(Type type, TypeKind kind) return "GetTypeDescription"; } } + + public class Issue4803Mutation + { + public Issue4803Result DoSomething(string userName) + => new() { UserName = userName }; + } + + public class Issue4803Result + { + public string UserName { get; set; } = null!; + } + + public class Issue4803NamingConvention : DefaultNamingConventions + { + public override string GetMemberName(MemberInfo member, MemberKind kind) + { + if (kind is MemberKind.ObjectField or MemberKind.InputObjectField) + { + return ToSnakeCase(member.Name); + } + + return base.GetMemberName(member, kind); + } + + public override string GetMemberName(string originalMemberName, MemberKind kind) + { + if (kind is MemberKind.ObjectField or MemberKind.InputObjectField) + { + return ToSnakeCase(originalMemberName); + } + + return base.GetMemberName(originalMemberName, kind); + } + + private static string ToSnakeCase(string memberName) + { + var pattern = new System.Text.RegularExpressions.Regex( + @"[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+"); + return string.Join("_", pattern.Matches(memberName)).ToLowerInvariant(); + } + } } diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/AnnotationBasedMutations.MutationConvention_With_SnakeCase_ObjectField_NamingConvention_Uses_PascalCase_TypeNames.graphql b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/AnnotationBasedMutations.MutationConvention_With_SnakeCase_ObjectField_NamingConvention_Uses_PascalCase_TypeNames.graphql new file mode 100644 index 00000000000..f84a94f5d3e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/__snapshots__/AnnotationBasedMutations.MutationConvention_With_SnakeCase_ObjectField_NamingConvention_Uses_PascalCase_TypeNames.graphql @@ -0,0 +1,19 @@ +schema { + mutation: Issue4803Mutation +} + +type DoSomethingPayload { + issue4803_result: Issue4803Result +} + +type Issue4803Mutation { + do_something(input: DoSomethingInput!): DoSomethingPayload! +} + +type Issue4803Result { + user_name: String! +} + +input DoSomethingInput { + user_name: String! +}