0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQLとEFにおける値オブジェクト作成・適用

Posted at

はじめに

DDD駆動設計を採用して値オブジェクトを作成する際に、EntitiyFrameworkHotChocolateの各コンバーターを適用する必要がある。値オブジェクトから汎用的にコンバーターを作成・適用することで開発効率を上げることを目的とする。
※概要については各リンクを参照のこと。

値オブジェクトについて

汎用的なクラスとするために、値オブジェクトの抽象インターフェース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);
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?