Add an improved annotation for polymorphic use of `OneOf`
Is your feature request related to a problem?
Yes! :slightly_smiling_face:
Currently the implementation of oneOf in Hot Chocolate doesn't offer any measurable convenience or even any degree of "polymorphism" as implied by the spec and much of the documentation.
From a GraphQL spec point of view, it's just a mutally exclusive assertion: That one out of a handful of fields is non-null.
When consuming oneOf functionality in Hot Chocolate, nothing is done to leverage the common interface and it's still up to the developer to figure out which of the numerous fields is non-null, thus requiring a small-but-gross amount of boilerplate.
(It's a little funny, the documentation has // Omitted code for brevity in the sections where the most of the actual benefit people expect with oneOf to happen.)
[OneOf]
public class PetInput
{
public Cat? Cat { get; set; }
public Dog? Dog { get; set; }
public Fish? Fish { get; set; }
}
public class Cat : IPet {}
public class Dog : IPet {}
public class Fish : IPet {}
public class Mutation
{
public Task<IPet> CreatePetAsync(PetInput input)
{
// This is what's being omitted for "brevity". Hot Chocolate can totally help avoid this kind of boilerplate code.
var pet = input.Cat ?? input.Dog ?? input.Fish ?? throw new("Unreachable thanks to `oneOf`.");
return pet;
}
}
Overall, I think Hot Chocolate can be of more assistance to the developer when the desired use of oneOf is for polymorphism.
The solution you'd like
In polymorphic scenarios, declaring InputType should be considered unnecessary. Hot Chocolate shouldn't require me to author something like a PetInput, but instead should be generating that type for the client automatically.
Accepting that the current OneOf exists and could still be useful in other scenarios, Hot Chocolate should introduce something that behaves largely like a union type, but when used for input, performs the work necessary to marshal a value of the correct type to the mutation:
[PolymorphicOneOf]
public interface IPet
{
}
public class Cat : IPet {}
public class Dog : IPet {}
public class Fish : IPet {}
public class Mutation
{
public Task<IPet> CreatePetAsync(IPet pet)
{
// No code needs to be omitted for brevity anymore, `pet` is an `IPet`, yay!
return pet;
}
}
Product
Hot Chocolate
well,
[PolymorphicOneOf]
public interface IPet
{
}
public class Cat : IPet {}
public class Dog : IPet {}
public class Fish : IPet {}
this does make naming of the fields difficult.
but we know that the api has to improve to avoid all the ??.
If we introduce a pattern we also have to take this into account:
[OneOf]
public class ScalarOneOf
{
public string? Foo { get; set; }
public int? Bar { get; set; }
public double? Baz { get; set; }
}
Maybe something like:
public class Mutation
{
public Task<IPet> CreatePetAsync(OneOf<PetInput> input)
{
return input.Get<IPet>(); // throws when not of type IPet
// or
if(input.TryGet(out IPet pet))
{
throw new("Unreachable thanks to `oneOf`.");
}
return pet;
// or
return input.Value switch {
IPet p => p,
_ => throw new("Unreachable thanks to `oneOf`.");
};
}
}
I like it (particularly option 3)! Being able to pull out a value based on interface would be pretty neat!
We were chatting about this in the morning 😅 so we will make it easier, it probably will make its way into 12.10
That include 13.x? :slightly_smiling_face:
@michaelstaib - Am I right in understanding that the OneOf type is the only value that can be accepted by the mutation when used? Or can I have other types alongside a OneOf decorated type?
public async Task<bool> MyDemoMutation(
MyOneOfSomethings oneOfSomething,
Guid someIdentifier
)
{
// ...
}
@atrauzzi oneof is only scoped for this input object
Good stuff. Anyway, still love the ideas from @PascalSenn. Would really offer a benefit!
A followup question...
Can I do a list/array of OneOf values with things as they are currently?
Not stale.
Need this feature because I have a domain model with nested OneOf. Such polymorphic way of doing things will save me from creating new classes.
class MyModel
{
public ICommonOperation { get; set; }
public string SomeProperty { get; set; }
}
With the way things are now I have to introduce new classes instead of just using MyModel for input
public class CommonOperationInput {
Operation1? Operation1 {get;set; }
Operation2? Operation2 {get; set; }
}
public class MyModelInput
{
public CommonOperationInput { get; set; }
public string SomeProperty { get; set; }
}
I have the exact same problem and concerns about this, like @Spaier we have a domain object with polymorphic objects or lists of those (f.e. different implementations of a base class). To get strongly typed schema we use union types, so we had to use one-of for the corresponding input objects. As one can not use middlewares on arguments or input objects (or can one?), i assume this has to be done somehow like Michael demonstrated in the youtube video on rewriting resolver arguments - but the example on simple strings does not get that far. Although announced, there isn't yet an episode on syntax rewriters for complex input objects.
It would be so so great to get some helper methods for this or at least an example on how to do this "the complicated way"..
And in addition to the proposed helpers by @PascalSenn: in my case the fields with polymorphic objects or object lists are somehow nested in a complex object and therefore not directly the root input argument. so i would prefer more like a value converter, directly converting between a union type and its corresponding one-of input, f.e. during configuration of the one-of input object; something neat like
public class MyOneOfInputType : InputObjectType<MyOneOfInput>
{
protected override void Configure(IInputObjectTypeDescriptor<MyOneOfInput> descriptor)
{
base.Configure(descriptor);
descriptor.OneOf()
// here the runtime type should be rewritten
.ConvertToUnionType<MyUnionType>(
// if not automatically possible expression or function parameter to do the conversion..
);
}
}
Also just ran into a situation where I want to be able to use a union type (or oneof) in a filter.