dotansimha / dotansimha/graphql-code-generator-community
[c-sharp] Add an implementation for GQL Union Types and Interfaces
- Dominant language
- TypeScript
- Stars
- 137
- Forks
- 195
- Avg merge
- 6h 20m
- Merged PRs (30d)
- 16
Description
**Is your feature request related to a problem? Please describe.**
The c-sharp plugin currently does not provide an implementation of GQL Union types and Interfaces. I will refer to both of these as Composition Types.
Consider the following schema definition:
```
union Vehicle = Airplane | Car
type Airplane {
wingspan: Int
}
type Car {
licensePlate: String
}
```
With the current implementation, these types are simply ignored. This makes the plugin not really usable for real-world scenarios, such as generating type for a complex API currently serving many real users.
**Describe the solution you'd like**
[On a personal fork of graphql-code-generator](https://github.com/cicciodm/graphql-code-generator/tree/c%23-union-types) I have implemented a way to model Composition Types in C#.
The language has no concept of Union Types, so I took inspiration from Typescript to model it. Using the above example, this is how it works.
The Union Type is modelled with an `interface` named as the type itself, and an `Enum` called `{name}Kind` with values representing each concrete implementation of the Union Type.
```
///
/// An enum representing the possible values of Vehicle
///
public enum VehicleKind {
///
/// Airplane
///
Airplane,
///
/// Car
///
Car
}
public interface Vehicle {
///
/// Kind is used to discriminate by type instances of this interface
///
VehicleKind Kind { get; }
}
```
Each of the concrete type will implement the generic interface, including an autogenerated `getter`, which will return the correct value of `Kind`.
```
#region Airplane
public class Airplane {
#region members
///
/// The kind used to discriminate the union type
///
VehicleKind Vehicle.Kind { get { return VehicleKind.Airplane; } }
[JsonProperty("wingspan")]
public int? wingspan { get; set; }
#endregion
}
#endregion
#region Car
public class Car {
#region members
///
/// The kind used to discriminate the union type
///
VehicleKind Vehicle.Kind { get { return VehicleKind.Car; } }
[JsonProperty("licensePlate")]
public string licensePlate { get; set; }
#endregion
}
#endregion
```
When faced with a `Veichle`, devs can then check on the `VeichleKind` on their object, before safely casting the object to the appropriate type.
```
Veichle genericVeichle = query.data.veichle;
if (genericVeichle.Kind == VeichleKind.Airplane)
{
var wingSpan = (genericVeichle as Airplane).wingspan;
}
```
Another block necessary to make this implementation work is the addition of a custom implementation for a Newtonsoft JSONConverter for correctly Deserializing fields that are of any Composition type (`UnionType` or `Interface`), as simply calling `serializer.Deserialize()` would otherwise fail with `Cannot instantiate item of type Veichle`
The Custom Converters (one for fields of `CompositionType` type, and one for list of `Composition Type` objects) work by inspecting the `__typename` of the object currently being deserialised. Based on the `__typename`, the appropriate Generated Type will be found using introspection, and an appropriate `ToObject` method will be created, with the correct type parameter.
```
///
/// Given __typeName returns JToken::ToObject[__typeName]. (via cache to improve performance)
///
/// The __typeName
/// JToken::ToObject[__typeName]
public static Func GetToObjectMethodForTargetType(string typeName)
{
if (!ToObjectForTypenameCache.ContainsKey(typeName))
{
// Get the type corresponding to the typename
Type targetType = typeof(YammerGQLTypes).Assembly
.GetTypes()
.ToList()
.Where(t => t.Name == typeName)
.FirstOrDefault();
// Create a parametrised ToObject method using targetType as
var method = typeof(JToken).GetMethods()
.Where(m => m.Name == "ToObject" && m.IsGenericMethod && m.GetParameters().Length == 0).FirstOrDefault();
var genericMethod = method.MakeGenericMethod(targetType);
var toObject = (Func)genericMethod.CreateDelegate(Expression.GetFuncType(typeof(JToken), typeof(object)));
ToObjectForTypenameCache[typeName] = toObject;
}
return ToObjectForTypenameCache[typeName];
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var loadedObject = JObject.Load(reader);
var typeNameObject = loadedObject["__typename"];
if (typeNameObject == null)
{
throw new JsonWriterException($"CompositionTypeConverter Exception: missing __typeName field when parsing {objectType.Name}. Requesting the __typename field is required for converting Composition Types");
}
var typeName = loadedObject["__typename"].Value();
var toObject = GetToObjectMethodForTargetType(typeName);
object objectParsed = toObject(loadedObject);
return objectParsed;
}
```
All fields that require the Custom Converter to be parsed correctly, are automatically marked with the `[JsonConverter(typeof(UnionType[List]Converter))]` attribute.
**Describe alternatives you've considered**
No alternative considered as of now. I have tried to push the CustomConverter tag to the types to be parsed, as opposed to the fields of those types, but that approach was not successful.
**Additional context**
A version of this implementation is currently being used in production, and has several hundred thousand hits per day, with near perfect success rate.
Link to the fork: https://github.com/cicciodm/graphql-code-generator/tree/c%23-union-types/packages/plugins/c-sharp/c-sharp/src
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.