From 45fc0b84641dd17b9cd0422d96724376bd605327 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 27 May 2026 07:14:44 +0000 Subject: [PATCH] Add escape hatch for mutation name formatting wuth mutation conventions --- .../MutationConventionTypeInterceptor.cs | 39 +++++++------ .../Configurations/CoreFieldFlags.cs | 3 +- .../ObjectFieldConfiguration.cs | 20 +++++++ .../AnnotationBasedMutations.cs | 57 +++++++++++++++++++ 4 files changed, 102 insertions(+), 17 deletions(-) diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs index 4ae4dd9bc05..8de71c209b4 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs @@ -1,3 +1,4 @@ +using System.Text; using HotChocolate.Features; using HotChocolate.Types.Helpers; using static HotChocolate.Resolvers.FieldClassMiddlewareFactory; @@ -207,7 +208,7 @@ private void TryApplyInputConvention( TypeMemHelper.Return(argumentNameMap); } - var inputTypeName = options.FormatInputTypeName(mutation.Name); + var inputTypeName = options.FormatInputTypeName(mutation); if (_typeRegistry.NameRefs.ContainsKey(inputTypeName)) { @@ -278,7 +279,7 @@ private void TryApplyPayloadConvention( Options options) { var typeRef = mutation.Type; - var payloadTypeName = options.FormatPayloadTypeName(mutation.Name); + var payloadTypeName = options.FormatPayloadTypeName(mutation); // we ensure that we can resolve the mutation result type. if (!_typeLookup.TryNormalizeReference(typeRef!, out typeRef) @@ -396,7 +397,7 @@ private void TryApplyPayloadConvention( // now that everything is put in place we will create the error types and // the error middleware. - var errorTypeName = options.FormatErrorTypeName(mutation.Name); + var errorTypeName = options.FormatErrorTypeName(mutation); RegisterErrorType(CreateErrorType(errorTypeName, errorDefinitions), mutation.Name); var errorListTypeRef = Parse($"[{errorTypeName}!]"); payloadTypeDef.Fields.Add( @@ -434,7 +435,7 @@ private void TryApplyPayloadConvention( if (errorDefinitions.Count > 0) { // create error type - var errorTypeName = options.FormatErrorTypeName(mutation.Name); + var errorTypeName = options.FormatErrorTypeName(mutation); RegisterErrorType(CreateErrorType(errorTypeName, errorDefinitions), mutation.Name); var errorListTypeRef = Parse($"[{errorTypeName}!]"); errorField = new FieldDef(options.PayloadErrorsFieldName, errorListTypeRef); @@ -864,34 +865,40 @@ private readonly ref struct Options( public bool Apply { get; } = apply ?? MutationConventionOptionDefaults.ApplyToAllMutations; - public string FormatInputTypeName(string mutationName) + public string FormatInputTypeName(ObjectFieldConfiguration mutation) => InputTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - FormatMutationName(mutationName)); + FormatMutationName(mutation)); - public string FormatPayloadTypeName(string mutationName) + public string FormatPayloadTypeName(ObjectFieldConfiguration mutation) => PayloadTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - FormatMutationName(mutationName)); + FormatMutationName(mutation)); - public string FormatErrorTypeName(string mutationName) + public string FormatErrorTypeName(ObjectFieldConfiguration mutation) => PayloadErrorTypeNamePattern.Replace( $"{{{MutationConventionOptionDefaults.MutationName}}}", - FormatMutationName(mutationName)); + FormatMutationName(mutation)); + + private static string FormatMutationName(ObjectFieldConfiguration mutation) + { + ArgumentNullException.ThrowIfNull(mutation); + + return mutation.DisableMutationReformatting + ? mutation.Name + : FormatMutationName(mutation.Name); + } private static string FormatMutationName(string mutationName) { - if (string.IsNullOrEmpty(mutationName)) - { - return mutationName; - } + ArgumentException.ThrowIfNullOrEmpty(mutationName); - if (mutationName.IndexOf('_', StringComparison.Ordinal) < 0) + if (!mutationName.Contains('_')) { return char.ToUpperInvariant(mutationName[0]) + mutationName[1..]; } - var builder = new System.Text.StringBuilder(mutationName.Length); + var builder = new StringBuilder(mutationName.Length); var upperNext = true; foreach (var c in mutationName) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/CoreFieldFlags.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/CoreFieldFlags.cs index c700eae18ec..a1b1a729216 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/CoreFieldFlags.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/CoreFieldFlags.cs @@ -47,5 +47,6 @@ internal enum CoreFieldFlags : long UsesProjections = 1L << 31, ImplicitField = 1L << 32, BatchResolver = 1L << 33, - MemberReplacement = 1L << 34 + MemberReplacement = 1L << 34, + DisableMutationReformatting = 1L << 35 } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/ObjectFieldConfiguration.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/ObjectFieldConfiguration.cs index d4b55ce5f8f..527ff3e2ef3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/ObjectFieldConfiguration.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Configurations/ObjectFieldConfiguration.cs @@ -208,6 +208,26 @@ public bool IsParallelExecutable } } + /// + /// If this field is a mutation to which mutation conventions are applied, + /// this flag indicates that the field name should not be reformatted. + /// + public bool DisableMutationReformatting + { + get => (Flags & CoreFieldFlags.DisableMutationReformatting) == CoreFieldFlags.DisableMutationReformatting; + set + { + if (value) + { + Flags |= CoreFieldFlags.DisableMutationReformatting; + } + else + { + Flags &= ~CoreFieldFlags.DisableMutationReformatting; + } + } + } + /// /// Defines in which DI scope this field is executed. /// diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs index 8606b232d7b..9a354bc8b18 100644 --- a/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/AnnotationBasedMutations.cs @@ -1319,6 +1319,32 @@ public async Task MutationConvention_With_SnakeCase_ObjectField_NamingConvention Assert.DoesNotContain("userName: String!", schemaText); } + [Fact] + public async Task MutationConvention_With_DisabledMutationReformatting_Uses_Field_Name() + { + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddMutationType() + .AddMutationConventions( + new MutationConventionOptions + { + ApplyToAllMutations = true, + InputTypeNamePattern = "{MutationName}InputType", + PayloadTypeNamePattern = "{MutationName}PayloadType" + }) + .ModifyOptions(o => o.StrictValidation = false) + .BuildSchemaAsync(); + + var schemaText = schema.ToString(); + + Assert.Contains("ch_myMutation(input: ch_myMutationInputType!): ch_myMutationPayloadType!", schemaText); + Assert.Contains("input ch_myMutationInputType {", schemaText); + Assert.Contains("type ch_myMutationPayloadType {", schemaText); + Assert.DoesNotContain("ChMyMutationInputType", schemaText); + Assert.DoesNotContain("ChMyMutationPayloadType", schemaText); + } + [Fact] public async Task Mutation_With_ErrorAnnotatedAndCustomInterface_LateAndEarlyRegistration() { @@ -1575,6 +1601,12 @@ public User DoSomething(string name) } } + [PrefixMutationFields("ch_")] + public class MutationWithDisabledReformatting + { + public string MyMutation(string value) => value; + } + public class SimpleMutationWithSingleError { [Error(typeof(CustomException))] @@ -2035,4 +2067,29 @@ private static string ToSnakeCase(string memberName) [System.Text.RegularExpressions.GeneratedRegex(@"[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+")] private static partial System.Text.RegularExpressions.Regex SnakeCasePatternRegex(); } + + public sealed class PrefixMutationFieldsAttribute(string prefix) : ObjectTypeDescriptorAttribute + { + protected override void OnConfigure( + IDescriptorContext context, + IObjectTypeDescriptor descriptor, + Type? type) + { + if (type is null) + { + return; + } + + descriptor + .Extend() + .OnBeforeCreate((_, definition) => + { + foreach (var field in definition.Fields) + { + field.Name = prefix + field.Name; + field.DisableMutationReformatting = true; + } + }); + } + } }