C#でカスタム属性(Attribute)を用いたプロパティの動的に代入してみる

2024/11/29に公開

はじめに

C#では、カスタム属性(Attribute,アノテーション)とリフレクションを組み合わせることで、クラスのプロパティを動的に操作することができます。この記事では、PersonDogといったクラスを例に、カスタム属性を用いて値オブジェクトのプロパティを動的に設定する汎用的なメソッドの作成方法を解説します。

カスタム属性の定義

まず、値オブジェクトに関連付けるためのカスタム属性を定義します。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ValueObjectAttribute : Attribute
{
    public string Name { get; }

    public ValueObjectAttribute(string name)
    {
        Name = name;
    }
}

このValueObjectAttributeは、プロパティに対して値オブジェクトの名前を関連付けるために使用します。

クラスの定義

次に、PersonクラスとDogクラスを定義します。Personクラスには、Dogという値オブジェクトを持たせ、そのプロパティを動的に設定します。

Dogクラス

public class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Personクラス

using System;
using System.Collections.Generic;
using System.Reflection;

public class Person
{
    private Dog _dog;

    [ValueObject("Dog")]
    public string DogName { get; set; }

    [ValueObject("Dog")]
    public int DogAge { get; set; }

    public void SetValueObjects()
    {
        // 現在のインスタンスの全てのプロパティを取得
        var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

        // 値オブジェクトとそれに関連付けられたプロパティを保持するディクショナリ
        var valueObjects = new Dictionary<string, object>();

        foreach (var prop in properties)
        {
            // プロパティに付与された ValueObjectAttribute を取得
            var attr = prop.GetCustomAttribute<ValueObjectAttribute>();
            if (attr != null)
            {
                // 値オブジェクトのインスタンスを取得または作成
                if (!valueObjects.TryGetValue(attr.Name, out var valueObject))
                {
                    // 値オブジェクトに対応するフィールドを取得(例:_dog)
                    var field = this.GetType().GetField($"_{attr.Name}", BindingFlags.NonPublic | BindingFlags.Instance);
                    if (field == null)
                    {
                        // フィールドが存在しない場合はスキップ
                        continue;
                    }

                    // 値オブジェクトが未初期化の場合は初期化
                    valueObject = field.GetValue(this) ?? Activator.CreateInstance(field.FieldType);
                    field.SetValue(this, valueObject);
                    valueObjects[attr.Name] = valueObject;
                }

                // 値オブジェクトのプロパティに値をセット
                var valueObjectProp = valueObject.GetType().GetProperty(prop.Name.Replace(attr.Name, ""), BindingFlags.Public | BindingFlags.Instance);
                if (valueObjectProp != null && valueObjectProp.CanWrite)
                {
                    var value = prop.GetValue(this);
                    valueObjectProp.SetValue(valueObject, value);
                }
            }
        }
    }
}

メソッドの解説

SetValueObjectsメソッドでは、以下の手順で値オブジェクトのプロパティを動的に設定しています。

  1. プロパティの取得: リフレクションを使用して、Personクラスの全てのパブリックプロパティを取得します。

  2. カスタム属性のチェック: 各プロパティに対して、ValueObjectAttributeが付与されているか確認します。

  3. 値オブジェクトの取得または作成: 属性で指定された名前の値オブジェクトが既に存在するか確認し、存在しなければプライベートフィールドから取得または新規作成します。

  4. プロパティのマッピング: Personクラスのプロパティから、対応する値オブジェクトのプロパティに値をセットします。

    • プロパティ名の変換が必要な場合は、prop.Name.Replace(attr.Name, "")のように処理します。

使用例

以下は、Personクラスを使用した例です。

public class Program
{
    public static void Main()
    {
        var person = new Person
        {
            DogName = "Pochi",
            DogAge = 5
        };

        person.SetValueObjects();

        // 値オブジェクトのフィールドにアクセス(リフレクションを使用)
        var dogField = typeof(Person).GetField("_dog", BindingFlags.NonPublic | BindingFlags.Instance);
        var dog = (Dog)dogField.GetValue(person);

        Console.WriteLine($"Dog's Name: {dog.Name}");
        Console.WriteLine($"Dog's Age: {dog.Age}");
    }
}

実行結果:

Dog's Name: Pochi
Dog's Age: 5

注意点

  • フィールド名の一致: 値オブジェクトのプライベートフィールド名(例:_dog)は、ValueObjectAttributeで指定した名前と一致させる必要があります。

  • プロパティ名の変換: 値オブジェクトのプロパティ名とPersonクラスのプロパティ名が異なる場合、適切に変換するロジックを実装する必要があります。

  • エラーハンドリング: リフレクションを使用するため、フィールドやプロパティが存在しない場合のエラーハンドリングを考慮する必要があります。

発展的な話題

  • パフォーマンスの最適化: リフレクションはパフォーマンスに影響を与える可能性があるため、頻繁に呼び出す場合はプロパティ情報をキャッシュするなどの対策が必要です。

  • ジェネリックな実装: 値オブジェクトの種類が増えても対応できるよう、ジェネリックを用いたより汎用的な実装を検討できます。

  • 検証ロジックの追加: 値をセットする際に、データの検証ロジックを追加することで、データ整合性を保つことができます。

まとめ

カスタム属性とリフレクションを組み合わせることで、クラスのプロパティから値オブジェクトのプロパティを動的に設定する汎用的なメソッドを作成できました。これにより、コードの再利用性と保守性を高めることができます。

Discussion