Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ public static SchemaTypeReference CreateTypeReference(
unwrapped = UnwrapListElementType(unwrapped) ?? unwrapped;
}

var flagsAttribute = compilation.GetTypeByMetadataName(WellKnownAttributes.FlagsAttribute);
if (flagsAttribute is not null && ContainsFlagsEnum(unwrapped, flagsAttribute))
{
return new SchemaTypeReference(
SchemaTypeReferenceKind.ExtendedTypeReference,
GetFullyQualifiedTypeName(unwrapped),
nullability: CreateNullabilityLiteral(unwrapped, IsNullable(unwrapped)));
}

// Next, we create a key that describes the type and ensures we are only executing the type factory once.
var (typeStructure, typeDefinition, nullability, isSimpleType) = CreateTypeKey(unwrapped);

Expand Down Expand Up @@ -220,6 +229,15 @@ private static void CollectNullability(
{
flags.Add(isNullable ? "true" : "false");

if (typeSymbol is IArrayTypeSymbol arrayType)
{
CollectNullability(
arrayType.ElementType,
IsNullable(arrayType.ElementType),
flags);
return;
}

if (typeSymbol is not INamedTypeSymbol namedType || !namedType.IsGenericType)
{
return;
Expand Down Expand Up @@ -328,6 +346,43 @@ private static bool IsArrayType(ITypeSymbol namedType, [NotNullWhen(true)] out I
return false;
}

private static bool ContainsFlagsEnum(
ITypeSymbol typeSymbol,
INamedTypeSymbol flagsAttribute)
{
if (typeSymbol is INamedTypeSymbol nullableType
&& nullableType.OriginalDefinition.SpecialType is SpecialType.System_Nullable_T)
{
return ContainsFlagsEnum(nullableType.TypeArguments[0], flagsAttribute);
}

if (typeSymbol is IArrayTypeSymbol arrayType)
{
return ContainsFlagsEnum(arrayType.ElementType, flagsAttribute);
}

if (typeSymbol is INamedTypeSymbol namedType
&& TryGetListElementType(namedType, out var elementType))
{
return ContainsFlagsEnum(elementType, flagsAttribute);
}

if (typeSymbol.TypeKind is not TypeKind.Enum)
{
return false;
}

foreach (var attribute in typeSymbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, flagsAttribute))
{
return true;
}
}

return false;
}

private static bool TryGetListElementType(
INamedTypeSymbol namedType,
[NotNullWhen(true)] out ITypeSymbol? elementType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static class WellKnownAttributes
public const string DescriptorAttribute = "HotChocolate.Types.DescriptorAttribute";
public const string GraphQLDeprecatedAttribute = "HotChocolate.GraphQLDeprecatedAttribute";
public const string ObsoleteAttribute = "System.ObsoleteAttribute";
public const string FlagsAttribute = "System.FlagsAttribute";
public const string BatchResolverAttribute = "HotChocolate.Types.BatchResolverAttribute";
public const string GraphQLTypeAttribute = "HotChocolate.GraphQLTypeAttribute";
public const string IsSelectedAttribute = "HotChocolate.Types.IsSelectedAttribute";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using SystemFlags = System.FlagsAttribute;

namespace HotChocolate.Types;

[SystemFlags]
public enum AnimalKind
{
Dog = 1,
Cat = 2
}

[Flags]
public enum FauxFlagsKind
{
First = 1,
Second = 2
}

[AttributeUsage(AttributeTargets.Enum)]
public sealed class FlagsAttribute : Attribute;

public class Animal
{
public required string Id { get; set; }

public required AnimalKind Kind { get; set; }
}

public class Zoo
{
public required string Id { get; set; }
}

[ObjectType<Zoo>]
public static partial class ZooType
{
public static AnimalKind PropertyAnimals
=> AnimalKind.Dog | AnimalKind.Cat;

public static AnimalKind GetAnimals([Parent] Zoo zoo)
=> AnimalKind.Dog | AnimalKind.Cat;

public static AnimalKind? GetNullableAnimals([Parent] Zoo zoo)
=> null;

public static Task<AnimalKind> GetTaskAnimalsAsync([Parent] Zoo zoo)
=> Task.FromResult(AnimalKind.Dog | AnimalKind.Cat);

public static ValueTask<AnimalKind> GetValueTaskAnimalsAsync([Parent] Zoo zoo)
=> ValueTask.FromResult(AnimalKind.Dog | AnimalKind.Cat);

public static AnimalKind?[]? GetAnimalArray([Parent] Zoo zoo)
=> [AnimalKind.Dog | AnimalKind.Cat, null];

public static List<AnimalKind> GetAnimalList([Parent] Zoo zoo)
=> [AnimalKind.Dog | AnimalKind.Cat];

public static List<List<AnimalKind>> GetNestedAnimalList([Parent] Zoo zoo)
=> [[AnimalKind.Dog | AnimalKind.Cat]];

public static Task<List<AnimalKind>> GetTaskAnimalListAsync([Parent] Zoo zoo)
=> Task.FromResult<List<AnimalKind>>([AnimalKind.Dog | AnimalKind.Cat]);

[BatchResolver]
public static ValueTask<List<AnimalKind>> GetBatchAnimalsAsync([Parent] List<Zoo> zoos)
=> ValueTask.FromResult<List<AnimalKind>>(
zoos.Select(_ => AnimalKind.Dog | AnimalKind.Cat).ToList());

public static FauxFlagsKind GetFauxFlags([Parent] Zoo zoo)
=> FauxFlagsKind.First;
}

public static partial class Query
{
public static Animal GetAnimal()
=> new() { Id = "1", Kind = AnimalKind.Dog | AnimalKind.Cat };

public static Zoo GetZoo()
=> new() { Id = "1" };

public static string FormatAnimalKinds(AnimalKind kinds)
=> kinds.ToString();

public static string FormatNullableAnimalKinds(AnimalKind? kinds)
=> kinds?.ToString() ?? "null";

public static string FormatAnimalKindList(List<AnimalKind?> kinds)
=> string.Join(",", kinds.Select(t => t?.ToString() ?? "null"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using HotChocolate.Execution;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Types;

public class FlagsEnumMethodBindingTests
{
[Fact]
public async Task FlagsEnum_Should_CoerceToFlagsObject_When_ReturnedFromPropertyBoundField()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.ModifyOptions(o => o.EnableFlagEnums = true)
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await executor.ExecuteAsync(
"{ animal { kind { isDog isCat } } }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"animal": {
"kind": {
"isDog": true,
"isCat": true
}
}
}
}
""");
}

[Fact]
public async Task FlagsEnum_Should_CoerceToFlagsObject_When_ReturnedFromMethodBoundField()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.ModifyOptions(o => o.EnableFlagEnums = true)
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

// act
var result = await executor.ExecuteAsync(
"{ zoo { animals { isDog isCat } } }",
TestContext.Current.CancellationToken);

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"zoo": {
"animals": {
"isDog": true,
"isCat": true
}
}
}
}
""");
}

[Fact]
public async Task FlagsEnum_Should_PreserveWrappers_When_ReturnedFromMethodBoundFields()
{
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.ModifyOptions(o => o.EnableFlagEnums = true)
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

var result = await executor.ExecuteAsync(
"""
{
zoo {
propertyAnimals {
isDog
isCat
}
nullableAnimals {
isDog
isCat
}
taskAnimals {
isDog
isCat
}
valueTaskAnimals {
isDog
isCat
}
animalArray {
isDog
isCat
}
animalList {
isDog
isCat
}
nestedAnimalList {
isDog
isCat
}
taskAnimalList {
isDog
isCat
}
batchAnimals {
isDog
isCat
}
fauxFlags
}
}
""",
TestContext.Current.CancellationToken);

result.MatchInlineSnapshot(
"""
{
"data": {
"zoo": {
"propertyAnimals": {
"isDog": true,
"isCat": true
},
"nullableAnimals": null,
"taskAnimals": {
"isDog": true,
"isCat": true
},
"valueTaskAnimals": {
"isDog": true,
"isCat": true
},
"animalArray": [
{
"isDog": true,
"isCat": true
},
null
],
"animalList": [
{
"isDog": true,
"isCat": true
}
],
"nestedAnimalList": [
[
{
"isDog": true,
"isCat": true
}
]
],
"taskAnimalList": [
{
"isDog": true,
"isCat": true
}
],
"batchAnimals": {
"isDog": true,
"isCat": true
},
"fauxFlags": "FIRST"
}
}
}
""");
}

[Fact]
public async Task FlagsEnum_Should_CoerceInput_When_UsedByGeneratedResolverMethods()
{
var executor = await new ServiceCollection()
.AddGraphQLServer()
.AddIntegrationTestTypes()
.AddPagingArguments()
.ModifyOptions(o => o.EnableFlagEnums = true)
.BuildRequestExecutorAsync(cancellationToken: TestContext.Current.CancellationToken);

var result = await executor.ExecuteAsync(
"""
{
formatAnimalKinds(kinds: { isDog: true, isCat: true })
formatNullableAnimalKinds
formatAnimalKindList(
kinds: [
{ isDog: true, isCat: false }
null
{ isDog: false, isCat: true }
])
}
""",
TestContext.Current.CancellationToken);

result.MatchInlineSnapshot(
"""
{
"data": {
"formatAnimalKinds": "Dog, Cat",
"formatNullableAnimalKinds": "null",
"formatAnimalKindList": "Dog,null,Cat"
}
}
""");
}
}
Loading
Loading