diff --git a/src/HotChocolate/Fusion/src/Fusion.Aspire/AspireCompositionHelper.cs b/src/HotChocolate/Fusion/src/Fusion.Aspire/AspireCompositionHelper.cs
index ebf7a116bb1..9bdc2bd764c 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Aspire/AspireCompositionHelper.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Aspire/AspireCompositionHelper.cs
@@ -213,16 +213,23 @@ internal static CompositionSettings CreateCompositionSettings(
{
CacheControlMergeBehavior = settings.CacheControlMergeBehavior,
EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification,
+ EnumValuesMergeBehavior = settings.EnumValuesMergeBehavior,
NodeResolution = settings.NodeResolution,
TagMergeBehavior = settings.TagMergeBehavior
},
- Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
+ Satisfiability =
+ {
+ IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths
+ },
ApolloFederationCompatibility =
{
AllowNonResolvableInterfaceObjects = settings.AllowNonResolvableInterfaceObjects,
ShareableFieldRuntimeTypeRouting = settings.ShareableFieldRuntimeTypeRouting
},
- Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
+ Preprocessor =
+ {
+ ExcludeByTag = settings.ExcludeByTag?.ToHashSet()
+ }
};
}
}
diff --git a/src/HotChocolate/Fusion/src/Fusion.Aspire/GraphQLCompositionSettings.cs b/src/HotChocolate/Fusion/src/Fusion.Aspire/GraphQLCompositionSettings.cs
index edb8eaac2e9..7cd5b684caa 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Aspire/GraphQLCompositionSettings.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Aspire/GraphQLCompositionSettings.cs
@@ -22,6 +22,12 @@ public struct GraphQLCompositionSettings
///
public bool? EnableGlobalObjectIdentification { get; set; }
+ ///
+ /// Gets or sets how enum values are merged when the same enum type is defined in multiple
+ /// source schemas.
+ ///
+ public EnumValuesMergeBehavior? EnumValuesMergeBehavior { get; set; }
+
///
/// Gets or sets how the gateway resolves the Query.node field.
///
diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs
index f376e54d757..13a697635f0 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Logging/LogEntryCodes.cs
@@ -44,6 +44,7 @@ public static class LogEntryCodes
public const string InterfaceObjectKeyMismatch = "INTERFACE_OBJECT_KEY_MISMATCH";
public const string InterfaceObjectKeyMissing = "INTERFACE_OBJECT_KEY_MISSING";
public const string InterfaceObjectNoInterface = "INTERFACE_OBJECT_NO_INTERFACE";
+ public const string InvalidEnumValuesMergeBehavior = "INVALID_ENUM_VALUES_MERGE_BEHAVIOR";
public const string InvalidFieldSharing = "INVALID_FIELD_SHARING";
public const string InvalidGraphQL = "INVALID_GRAPHQL";
public const string InvalidApolloFederationSupportSettings =
diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Options/EnumValuesMergeBehavior.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Options/EnumValuesMergeBehavior.cs
new file mode 100644
index 00000000000..4ad40f58052
--- /dev/null
+++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Options/EnumValuesMergeBehavior.cs
@@ -0,0 +1,25 @@
+namespace HotChocolate.Fusion.Options;
+
+///
+/// Defines how enum values are merged when the same enum type is defined in multiple source
+/// schemas.
+///
+public enum EnumValuesMergeBehavior
+{
+ ///
+ /// Applies , unless at least one Apollo Federation connector source
+ /// schema is part of the composition, in which case applies.
+ ///
+ Auto = 0,
+
+ ///
+ /// Enum types with the same name must define identical value sets.
+ ///
+ Strict = 1,
+
+ ///
+ /// Values of enums used only in output positions are merged by union. Enums used in any
+ /// input position still require identical value sets.
+ ///
+ Union = 2
+}
diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Options/SourceSchemaMergerOptions.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Options/SourceSchemaMergerOptions.cs
index 1462b2df2c5..fbdbfd389a2 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Composition/Options/SourceSchemaMergerOptions.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Options/SourceSchemaMergerOptions.cs
@@ -20,6 +20,12 @@ public sealed class SourceSchemaMergerOptions
///
public bool EnableGlobalObjectIdentification { get; set; }
+ ///
+ /// Defines how enum values are merged when the same enum type is defined in multiple source
+ /// schemas.
+ ///
+ public EnumValuesMergeBehavior EnumValuesMergeBehavior { get; set; } = EnumValuesMergeBehavior.Auto;
+
///
/// Defines how the gateway resolves the Query.node field.
///
diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/PreMergeValidationRules/EnumValuesMismatchRule.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/PreMergeValidationRules/EnumValuesMismatchRule.cs
index cdf80f6bdf8..96c605067dd 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Composition/PreMergeValidationRules/EnumValuesMismatchRule.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Composition/PreMergeValidationRules/EnumValuesMismatchRule.cs
@@ -1,7 +1,12 @@
using System.Collections.Immutable;
+using HotChocolate.Fusion.ApolloFederation;
using HotChocolate.Fusion.Events;
using HotChocolate.Fusion.Events.Contracts;
using HotChocolate.Fusion.Extensions;
+using HotChocolate.Fusion.Info;
+using HotChocolate.Fusion.Options;
+using HotChocolate.Types;
+using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.Logging.LogEntryHelper;
namespace HotChocolate.Fusion.PreMergeValidationRules;
@@ -19,21 +24,36 @@ namespace HotChocolate.Fusion.PreMergeValidationRules;
/// inconsistencies by enforcing that all instances of the same named enum across schemas have an
/// exact match in their values.
///
+///
+/// The configured merge behavior is provided at construction. Under
+/// , values of enums used only in output positions
+/// merge by union. applies Union when at least one
+/// Apollo Federation connector source schema is part of the composition, otherwise Strict.
+///
///
///
/// Specification
///
-internal sealed class EnumValuesMismatchRule : IEventHandler
+internal sealed class EnumValuesMismatchRule(EnumValuesMergeBehavior enumValuesMergeBehavior)
+ : IEventHandler
{
+ private readonly EnumValuesMergeBehavior _enumValuesMergeBehavior = enumValuesMergeBehavior;
+
public void Handle(EnumTypeGroupEvent @event, CompositionContext context)
{
- var (_, enumGroup) = @event;
+ var (typeName, enumGroup) = @event;
if (enumGroup.Length < 2)
{
return;
}
+ if (ResolveMergeBehavior(context) is EnumValuesMergeBehavior.Union
+ && IsOutputOnlyEnum(typeName, enumGroup))
+ {
+ return;
+ }
+
var enumValues = enumGroup
.SelectMany(e => e.Type.Values.AsEnumerable())
.Where(v => !v.IsInaccessible)
@@ -51,4 +71,82 @@ public void Handle(EnumTypeGroupEvent @event, CompositionContext context)
}
}
}
+
+ private EnumValuesMergeBehavior ResolveMergeBehavior(CompositionContext context)
+ {
+ if (_enumValuesMergeBehavior is not EnumValuesMergeBehavior.Auto)
+ {
+ return _enumValuesMergeBehavior;
+ }
+
+ foreach (var schema in context.SchemaDefinitions)
+ {
+ if (schema.Features.Get()?.Kind == "ApolloFederation")
+ {
+ return EnumValuesMergeBehavior.Union;
+ }
+ }
+
+ return EnumValuesMergeBehavior.Strict;
+ }
+
+ private static bool IsOutputOnlyEnum(string typeName, ImmutableArray enumGroup)
+ {
+ foreach (var (_, schema) in enumGroup)
+ {
+ if (IsUsedInInputPosition(typeName, schema))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool IsUsedInInputPosition(string typeName, MutableSchemaDefinition schema)
+ {
+ foreach (var type in schema.Types)
+ {
+ switch (type)
+ {
+ case MutableComplexTypeDefinition complexType:
+ foreach (var field in complexType.Fields)
+ {
+ foreach (var argument in field.Arguments)
+ {
+ if (argument.Type.NamedType().Name == typeName)
+ {
+ return true;
+ }
+ }
+ }
+
+ break;
+
+ case MutableInputObjectTypeDefinition inputType:
+ foreach (var field in inputType.Fields)
+ {
+ if (field.Type.NamedType().Name == typeName)
+ {
+ return true;
+ }
+ }
+
+ break;
+ }
+ }
+
+ foreach (var directiveDefinition in schema.DirectiveDefinitions)
+ {
+ foreach (var argument in directiveDefinition.Arguments)
+ {
+ if (argument.Type.NamedType().Name == typeName)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs
index 811ba2397a0..0b6924fe691 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs
@@ -36,6 +36,12 @@ public SchemaComposer(
public CompositionResult Compose()
{
+ if (!Enum.IsDefined(_schemaComposerOptions.Merger.EnumValuesMergeBehavior))
+ {
+ return InvalidEnumValuesMergeBehavior(
+ $"The enum values merge behavior '{_schemaComposerOptions.Merger.EnumValuesMergeBehavior}' is invalid.");
+ }
+
if (!Enum.IsDefined(_schemaComposerOptions.Merger.NodeResolution))
{
return InvalidNodeResolution(
@@ -159,7 +165,8 @@ public CompositionResult Compose()
// Pre Merge Validation
var preMergeValidationResult =
- new PreMergeValidator(schemas, PreMergeRules, _log).Validate();
+ new PreMergeValidator(schemas, CreatePreMergeRules(_schemaComposerOptions), _log)
+ .Validate();
if (preMergeValidationResult.IsFailure)
{
@@ -207,6 +214,18 @@ public CompositionResult Compose()
return mergedSchema;
}
+ private CompositionError InvalidEnumValuesMergeBehavior(string message)
+ {
+ _log.Write(
+ LogEntryBuilder.New()
+ .SetMessage(message)
+ .SetCode(LogEntryCodes.InvalidEnumValuesMergeBehavior)
+ .SetSeverity(LogSeverity.Error)
+ .Build());
+
+ return new CompositionError(message);
+ }
+
private CompositionError InvalidNodeResolution(string message)
{
_log.Write(
@@ -276,12 +295,12 @@ private CompositionError InvalidShareableFieldRuntimeTypeRouting(string message)
];
///
- /// Gets the rules that run across the source schemas before the merge, in the order they are
- /// applied.
+ /// Creates the rules that run across the source schemas before the merge, in the order they
+ /// are applied, configured from the given options.
///
- internal static ImmutableArray