はじめに
DDD駆動設計を採用して値オブジェクトを作成する際に、EntitiyFramework・HotChocolateの各コンバーターを適用する必要がある。値オブジェクトから汎用的にコンバーターを作成・適用することで開発効率を上げることを目的とする。
※概要については各リンクを参照のこと。
値オブジェクトについて
汎用的なクラスとするために、値オブジェクトの抽象インターフェースIValueObject.csを実装する。※例として、メールアドレスの値オブジェクトMailAddress.csを記述する。
IValueObject.cs
// 値オブジェクトを示すインターフェース
public interface IValueObject<TValueObject, TValue> : IValueObjectBase<TValue>
where TValueObject : IValueObject<TValueObject, TValue>
{
// 値
public new TValue Value { get; }
}
MailAddress.cs
// メールアドレスを表す値オブジェクト
[ValueObject]
public readonly record struct MailAddress : IValueObject<MailAddress, string>
{
// メールアドレス
public string Value { get; }
private MailAddress(string value) => this.Value = value;
// 作成&バリデーションチェック
public static MailAddress Create(string value)
{
... // 値オブジェクト単位でバリデーションチェックを実装できる
return new(value);
}
// MailAddress から string への暗黙的な変換を定義します。
public static implicit operator string(MailAddress mailAddress)
=> return mailAddress.Value;
}
EntityFrameworkのコンバーター作成・適用
EntityFramerokは、データ←→モデルクラスの変換処理にSystem.Text.Jsonを使用しており、専用のコンバーターValueObjectJsonConverter.csを作成して、ValueObjectAttribute.csを付与して適用する必要がある。
ValueObjectAttribute.cs
// 値オブジェクト属性
public class ValueObjectAttribute : JsonConverterAttribute
// 値オブジェクト変換クラス作成
public override JsonConverter? CreateConverter(Type valueObjectType)
{
// IValueObject<T> の T を取得する
var valueType = valueObjectType.GetInterface(typeof(IValueObject<,>).Name)!.GetGenericArguments()[1];
// 汎用コンバータの型を動的に作る
var converterType = typeof(ValueObjectJsonConverter<,>).MakeGenericType(valueObjectType, valueType);
// インスタンスを生成して返す
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
}
ValueObjectJsonConverter.cs
// IValueObjectを実装したクラスを汎用的にJSON変換するためのコンバータ
public class ValueObjectJsonConverter<TValueObject, TValue> : JsonConverter<TValueObject>
where TValueObject : IValueObject<TValueObject, TValue>
{
// ValueObject -> Value
public override TValueObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = JsonSerializer.Deserialize<TValue>(ref reader, options);
var constructor = typeof(TValueObject).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
[typeof(TValue)],
null);
return (TValueObject)constructor.Invoke([value]);
}
// Value -> ValueObject
public override void Write(Utf8JsonWriter writer, TValueObject valueObject, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, valueObject.Value, options);
}
HotChocolateのコンバーター作成・適用
コンバーターの作成・適用は「スキーマ定義モデルコンバーターAddValueObjectsExtension.cs」「フィルター定義モデルコンバーターAddValueObjectFilterConventionExtension.cs」の2パターン実装して、サービス登録時に実行する必要がある。
AddValueObjectsExtension.cs
// 値オブジェクト[ValueObject]を登録する拡張メソッド定義クラス
public static class AddValueObjectsExtension
{
// 値オブジェクトのスキーマ登録
public static IRequestExecutorBuilder AddValueObjects(this IRequestExecutorBuilder builder)
{
var valueObjectTypes = typeof(IValueObject<,>).Assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValueObject<,>)) && !t.IsAbstract);
foreach (var valueObjectType in valueObjectTypes)
{
var valueObjectInterface = valueObjectType.GetInterfaces().Single(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValueObject<,>));
var valueType = valueObjectInterface.GetGenericArguments()[1];
// 1. バインド設定
var graphQLType = GetGraphQLSchemaType(valueType);
builder.BindRuntimeType(valueObjectType, graphQLType);
if (valueObjectType.IsValueType)
{ // null許容型でも登録する
var nullableType = typeof(Nullable<>).MakeGenericType(valueObjectType);
builder.BindRuntimeType(nullableType, graphQLType);
}
// 2. 変換タイプ登録 : TValue <-> TValueObject の相互変換ロジックを登録
// ※各変換ロジックは、IChangeTypeProviderを継承したうえで、Valueプロパティを暗黙的に変換する。
var toValueObjectType = typeof(ToValueObjectChangeTypeProvider<,>).MakeGenericType(valueObjectType, valueType);
builder.Services.AddSingleton(typeof(IChangeTypeProvider), Activator.CreateInstance(toValueObjectType)!);
var fromValueObjectType = typeof(FromValueObjectChangeTypeProvider<,>).MakeGenericType(valueObjectType, valueType);
builder.Services.AddSingleton(typeof(IChangeTypeProvider), Activator.CreateInstance(fromValueObjectType)!);
}
return builder;
}
// TValueの型に応じて適切なGraphQL Typeを返すヘルパーメソッド
private static Type GetGraphQLSchemaType(Type tValue)
{
if (tValue == typeof(string))
return typeof(StringType);
}
}
AddValueObjectFilterConventionExtension.cs
// 値オブジェクトのフィルターコンバーター適用
public static class AddValueObjectFilterConventionExtension
{
// 値オブジェクト(ValueObject)の拡張操作を定義する
public static IFilterConventionDescriptor AddValueObjectFilterConvention(this IFilterConventionDescriptor descriptor)
{
// ValueObjectバインド
var valueObjectTypes = typeof(IValueObject<,>).Assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValueObject<,>)) && !t.IsAbstract);
foreach (var valueObjectType in valueObjectTypes)
{
var valueObjectInterface = valueObjectType.GetInterfaces().Single(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValueObject<,>));
var valueType = valueObjectInterface.GetGenericArguments()[1];
var graphQLFilter = GetGraphQLFilterType(valueType);
descriptor.BindRuntimeType(valueObjectType, graphQLFilter);
if (valueObjectType.IsValueType)
{ // null許容型も登録する必要がある。
var nullableType = typeof(Nullable<>).MakeGenericType(valueObjectType);
descriptor.BindRuntimeType(nullableType, graphQLFilter);
}
}
// 各ハンドラー追加
// ※ハンドラは既存のクエリハンドラを継承したうえで、Valueプロパティを暗黙的に変換する
descriptor.AddStringVoFilterConvention();
return descriptor;
}
// TValueの型に応じて適切なGraphQL Typeを返すヘルパーメソッド
private static Type GetGraphQLFilterType(Type tValue)
{
if (tValue == typeof(string))
return typeof(StringVoOperationFilterInputType);
}
}