【C#】独自の属性(Attribute)を定義してリフレクションで動作を制御する方法

C#では、標準で用意されている属性([Obsolete][Serializable] など)以外に、開発者が独自の属性(カスタム属性)を定義することができます。

これを利用すると、クラスやプロパティに対して独自のメタデータ(「この項目は必須」「この項目は自動変換する」などのマーク)を付与し、実行時にリフレクションを使ってその情報を読み取り、特別な処理を行うシステムを構築できます。

目次

カスタム属性の実装例

ここでは、Webサービスの「ユーザープロフィール」クラスを題材にします。 特定のプロパティに対し、「値がnullの場合は自動的に空文字(“”)に変換する」 という処理を行うための属性 [AutoEmpty] を自作し、その属性が付いているプロパティだけを対象に値を書き換える処理を実装します。

サンプルコード

using System;
using System.Reflection;

public class Program
{
    public static void Main()
    {
        // 1. テストデータの作成
        // Webサイトや自己紹介は未入力(null)の状態
        var user = new UserProfile
        {
            UserName = "GuestUser001",
            WebsiteUrl = null,
            Bio = null,
            PrivateMemo = null
        };

        Console.WriteLine("--- 処理前 ---");
        user.PrintState();

        // 2. 独自の属性が付いている箇所だけnullを空文字に変換
        ApplyNullToEmptyRule(user);

        Console.WriteLine("\n--- 処理後 ---");
        user.PrintState();
    }

    /// <summary>
    /// オブジェクト内のプロパティを走査し、[AutoEmpty]属性が付与され、
    /// かつ有効(Enabled=true)な場合に限り、nullを空文字に置換する
    /// </summary>
    public static void ApplyNullToEmptyRule<T>(T obj)
    {
        if (obj == null) return;

        Type type = typeof(T);
        
        // プロパティ一覧を取得
        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (var prop in properties)
        {
            // 文字列型以外は対象外
            if (prop.PropertyType != typeof(string)) continue;
            
            // 書き込み不可のプロパティはスキップ
            if (!prop.CanWrite) continue;

            // 3. カスタム属性 [AutoEmpty] を取得
            var attr = prop.GetCustomAttribute<AutoEmptyAttribute>();

            // 属性が付いていない、または属性の設定で無効化(Enabled=false)されている場合はスキップ
            if (attr == null || !attr.Enabled)
            {
                continue;
            }

            // 現在の値がnullであれば、空文字をセットする
            var currentValue = prop.GetValue(obj);
            if (currentValue == null)
            {
                prop.SetValue(obj, string.Empty);
                Console.WriteLine($"[変換実行] {prop.Name} を空文字に変換しました。");
            }
        }
    }
}

// ---------------------------------------------------------
// 1. 独自の属性クラスを定義
// Attributeクラスを継承し、名前は "~Attribute" にするのが慣例
// ---------------------------------------------------------
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AutoEmptyAttribute : Attribute
{
    // 変換を有効にするかどうかのフラグ
    public bool Enabled { get; }

    // コンストラクタ
    public AutoEmptyAttribute(bool enabled = true)
    {
        Enabled = enabled;
    }
}

// ---------------------------------------------------------
// 2. 属性を利用するデータクラス
// ---------------------------------------------------------
public class UserProfile
{
    // 属性なし:変換対象外
    public string UserName { get; set; }

    // 属性あり(true):nullなら空文字に変換される
    [AutoEmpty(true)]
    public string WebsiteUrl { get; set; }

    // 属性あり(引数なしはデフォルトtrue):変換される
    [AutoEmpty]
    public string Bio { get; set; }

    // 属性なし:nullのまま維持されるべき項目
    public string PrivateMemo { get; set; }

    public void PrintState()
    {
        Console.WriteLine($"Name   : <{UserName ?? "null"}>");
        Console.WriteLine($"Web    : <{WebsiteUrl ?? "null"}>");
        Console.WriteLine($"Bio    : <{Bio ?? "null"}>");
        Console.WriteLine($"Memo   : <{PrivateMemo ?? "null"}>");
    }
}

実行結果

--- 処理前 ---
Name   : <GuestUser001>
Web    : <null>
Bio    : <null>
Memo   : <null>

[変換実行] WebsiteUrl を空文字に変換しました。
[変換実行] Bio を空文字に変換しました。

--- 処理後 ---
Name   : <GuestUser001>
Web    : <>
Bio    : <>
Memo   : <null>

解説と技術的なポイント

1. Attributeクラスの継承

独自の属性を作るには、System.Attribute クラスを継承します。クラス名は慣例として末尾に Attribute を付けます(例: AutoEmptyAttribute)。使用する際は [AutoEmpty] のように末尾を省略して記述できます。

2. AttributeUsage 属性

[AttributeUsage] を使用して、そのカスタム属性が「どこに付けられるか」を制限します。

  • AttributeTargets.Property: プロパティにのみ付与可能にします。クラスやメソッドに誤って付けようとするとコンパイルエラーになります。
  • AllowMultiple = false: 同じプロパティにこの属性を複数回付けることを禁止します。

3. リフレクションによる読み取り

prop.GetCustomAttribute<T>() を使用して属性を取得します。

  • 戻り値が null の場合:そのプロパティには属性が付与されていません。
  • 戻り値がインスタンスの場合:属性が付与されており、コンストラクタで設定した値(Enabled プロパティなど)を読み取って処理の条件分岐に使用できます。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次