Version Tag: v2.4.0
Corresponding Source file: https://github.com/graphql-dotnet/graphql-dotnet/blob/v2.4.0/src/GraphQL/Types/EnumerationGraphType.cs#L37
Havent tested the bug on the newest version but the bug could be also be there.
When an enum is serialized, which has several flags, graphql-net returns null.
My workaround was creating a class that derived from EnumerationGraphType
Here is the source:
public class CustomEnumGraphType<TEnum> : EnumerationGraphType<TEnum>
{
public override object Serialize(object value)
{
var result = base.Serialize(value);
if (result != null)
{
return result;
}
if (value is Enum enumValue)
{
var enumNames = Enum.GetValues(enumValue.GetType()).Cast<Enum>()
.Where(x => enumValue.HasFlag(x))
.Select(x => base.Serialize(x));
return string.Join("|", enumNames);
}
return null;
}
}
Hopefully this will help you solving that bug :)
Have a nice day!
This is not a bug. Once I asked myself this question and wanted to add support for flags enums. I gradually realized that this was impossible. .NET flag enum contradicts the concept of GraphQL enum. These are just two different worlds. Also If you read the specification, then you will see that your implementation violates it.
@JKamsker @Shane32 I added this issue to the known issues.
A single flag enum in .Net could accurately be represented as a list of enums. e.g. ListGraphType<NonNullGraphType<MyEnumGraphType>>. I don't know how practical it would be to include this conversion in the base graphql library.
Thanks @sungam3r for putting it in the known issues list. I'll include this in the release notes.
Right. We can talk about some sort of conversions, maybe. But in general flag enums can never become graphql first-class citizens.
Here's a snippet that could be used to convert enum flag fields to lists of graphql enum values: (works for nullable or non-nullable types)
public static class EnumExtensions
{
public static IEnumerable<T> FromFlags<T>(this T value) where T : struct, Enum
{
return Enum.GetValues(typeof(T))
.Cast<T>()
.Distinct()
.Where(x => value.HasFlag(x));
}
public static IEnumerable<T> FromFlags<T>(this T? value) where T : struct, Enum
{
if (!value.HasValue)
return null;
return Enum.GetValues(typeof(T))
.Cast<T>()
.Distinct()
.Where(x => value.Value.HasFlag(x));
}
}
Since the serializer hasn't changed, there's no way to serialize two different flags that represent the same value, hence the reason for .Distinct(). In the future perhaps this could be built into graphql, where any enum being converted to a list of graphql enum types would automatically be converted, properly supporting multiple enum members representing the same value.
Assuming you've registered the enum graph type in the GraphTypeTypeRegistry, you can include the field in your graphs in this fashion:
Field("appearsIn", source => source.AppearsIn.FromFlags());
Or if you explicitly set the graph types, perhaps like this:
Field<NonNullGraphType<ListGraphType<NonNullGraphType<AppearsInEnumGraphType>>>>("appearsIn",
description: "Which movie they appear in.",
resolve: context => context.Source.AppearsIn.FromFlags());
I tested this code with the 3.0 beta, but it should be the same with 2.4.0. This of course only covers output types. Here's an untested rough draft of something for input types, but it needs review:
public static T? CombineFlags<T>(this IEnumerable<T> values) where T : struct, Enum
{
switch (values)
{
case null:
return null;
case IEnumerable<int> list:
return (T)(object)list.Aggregate((a, b) => a | b);
case IEnumerable<uint> list:
return (T)(object)list.Aggregate((a, b) => a | b);
case IEnumerable<long> list:
return (T)(object)list.Aggregate((a, b) => a | b);
case IEnumerable<ulong> list:
return (T)(object)list.Aggregate((a, b) => a | b);
default:
throw new NotSupportedException("Enum type not supported");
}
}
This of course only covers output types.
It is very important. I was thinking about flag enums working on input and output. In the end, I came to the conclusion that I should not try since the resulting solution is very fragile and violates the GraphQL enums design. If someone needs to use flags, then he can do it with ListGraphType<>.
Sounds reasonable. If we can come up with some solid workarounds (perhaps like the above), we can include it with the known issues / release notes, similar to the scoped dependency known issue.
Or, perhaps the helper functions can be provided within the graphql package, as they do not rely on third-party dependencies.
@Shane32 could you give me an example of how your input helper CombineFlags might be used? I'm struggling to get that working with a DTO of the underlying flag enum type.
Something like this (untested):
FieldAsync<NonNullGraphType<ResultGraph>>("Example",
resolve: async context =>
{
var values = context.GetArgument<IEnumerable<MyEnum>>("values");
var valuesCombined = values.CombineFlags();
return callDb(valuesCombined);
},
arguments: new QueryArguments(
new QueryArgument(typeof(NonNullGraphType<ListGraphType<NonNullGraphType<MyEnumGraphType>>>)) { Name = "values" }
));
Thanks @Shane32 appreciate it. I have been trying to use it on a field that is part of an 'input' type which hasn't been very successful so far. Think I may have to just stop exposing that as a flag enum in general, its getting too messy.
As part of an input type, you should be able to define your input like this:
public class UserInputGraph : InputObjectGraphType<UserInputModel>
{
public UserInputGraph()
{
Field(x => x.Name);
Field(x => x.Permissions);
}
}
public class UserInputModel
{
public string Name { get; set; }
public IEnumerable<MyEnum> Permissions { get; set; }
}
Note that you need to have an input model; you can't use the DTO object as the fields definitions don't line up. After GetArgument then you can copy the values into your DTO model, combining the flags from the enumerable into a single field at that time.
Thanks @Shane32 yes that makes sense - have been getting away without a model at the moment as they're 1:1 - time for that to change I think!
For my own projects, I find that the DTO models can typically be the basis of "output graphs", but for input graphs, I almost always need to create a corresponding input model for each input graph. I think this due to the fact that for an output graph, you can format/parse the data within the resolve parameter of each field, whereas input graphs simply represent a mapping to an input model, with no ability to add actual "code". (If there is a way, I'm not familiar with it.)
Yes, that is my inference as well. There's less flexibility 'on the way in'. Thanks for the advice!
whereas input graphs simply represent a mapping to an input model, with no ability to add actual "code"
it is realizable, I thought about it, but it will require a significant design changes
I'm trying to pass a list of enum values like mentioned above, but I cannot find out how to pass them correctly in the JSON GraphQL request.
I tried it like this:
{
"query": "query ($values: [MyEnum!]!) { exampleFunction(Example: $Example) { ... } }",
"variables": {
"Example": [ {"MyEnum": "MyEnumValue1"}, {"MyEnum": "MyEnumValue2"} ],
}
}
But I just get an error message back:
"Variable '$Example[0]' is invalid. Invalid Scalar value for input field."
What is the correct way to pass a list of enum values as a variable?
Thanks in advance for any tip on this
Andreas
Probably something more like this:
"Example": [ "MY_ENUM_VALUE_1", "MY_ENUM_VALUE_2" ]
Does that work? Note that you鈥檒l need to use the converted value names which will look something like that.
You鈥檒l probably also need to use "values" instead of "Example" because you defined the variable as $values
And you need to reference values where it says Example: $values
{
"query": "query ($values: [MyEnum!]!) { exampleFunction(Example: $values) { ... } }",
"variables": {
"values": [ "MY_ENUM_VALUE_1", "MY_ENUM_VALUE_2" ]
}
}
Thanks a lot for your super fast response, Shane, that's much appreciated!
And you're fully right, in fact I did try to write pass it just with this array notation before already, but I was simply not aware that the enum values get converted from e. g. MyEnumValue1 to MY_ENUM_VALUE_1. So changing them to this and using the simple array notation [ "MY_ENUM_VALUE_1", "MY_ENUM_VALUE_2" ] made it work immediately.
Sorry about that, it was just the first time I was trying to use enums as input variables.
I should have read the documentation more closely first.
Thanks again for your very quick support!
Andreas
... just as an additional note if somebody else ends up here as well:
For me the CombineFlags extension method above did not work.
I'm using this method from the StackOverflow reply here instead:
https://stackoverflow.com/a/53637933/701836
This seems to work fine for me.
Closed as answered.
Most helpful comment
Here's a snippet that could be used to convert enum flag fields to lists of graphql enum values: (works for nullable or non-nullable types)
Since the serializer hasn't changed, there's no way to serialize two different flags that represent the same value, hence the reason for
.Distinct(). In the future perhaps this could be built into graphql, where any enum being converted to a list of graphql enum types would automatically be converted, properly supporting multiple enum members representing the same value.Assuming you've registered the enum graph type in the
GraphTypeTypeRegistry, you can include the field in your graphs in this fashion:Or if you explicitly set the graph types, perhaps like this:
I tested this code with the 3.0 beta, but it should be the same with 2.4.0. This of course only covers output types. Here's an untested rough draft of something for input types, but it needs review: