dotansimha / dotansimha/graphql-code-generator-community

[c-sharp] Add an implementation for GQL Union Types and Interfaces

Open
#149 4 comments 5 reactions 0 assignees View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.