C# でカスタム属性を自作する

.NET には多くの組み込み属性が用意されているが、プロジェクト固有の要件に合った属性が欲しくなることもある。C# では System.Attribute を継承したクラスを定義するだけで、独自の属性を作れる。

最小のカスタム属性

カスタム属性は Attribute クラスを継承して作る。クラス名の末尾は慣例として Attribute を付ける。

using System;

public class AuthorAttribute : Attribute
{
    public string Name { get; }

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

使うときは末尾の Attribute を省略できる。

[Author("田中太郎")]
public class ReportGenerator
{
    public void Generate()
    {
        Console.WriteLine("レポート生成中...");
    }
}

これでクラスに「誰が作ったか」というメタデータを付与できた。属性自体はプログラムの動作を変えないが、リフレクションで読み取ることで実行時に活用できる。

AttributeUsage で適用先を制限する

デフォルトではカスタム属性はあらゆる要素に付けられるが、AttributeUsage 属性を使えば適用先を限定できる。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }

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

この定義では Author 属性はクラスとメソッドにしか付けられない。フィールドやプロパティに付けるとコンパイルエラーになる。

AttributeTargets.Class

クラスに適用可能にする。

AttributeTargets.Method

メソッドに適用可能にする。

AttributeTargets.Property

プロパティに適用可能にする。

AttributeTargets.All

すべての要素に適用可能にする(デフォルト)。

複数の対象を許可したい場合はビット OR 演算子 | で組み合わせる。適用先を明確にしておくと、意図しない場所に属性が付けられるミスを防げる。

複数回の適用を許可する

デフォルトでは同じ属性を 1 つの要素に複数回付けられない。AllowMultiple = true を指定すると、この制限が解除される。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }

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

[Author("田中太郎")]
[Author("鈴木花子")]
public class CollaborativeReport
{
    // 共同作成したクラス
}

共同執筆のような場面では AllowMultiple = true が自然な選択になる。

名前付きプロパティを追加する

コンストラクタ引数だけでなく、省略可能な名前付きプロパティも定義できる。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Email { get; set; } = "";
    public string Version { get; set; } = "1.0";

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

コンストラクタ引数は必須パラメータ、公開プロパティは省略可能なパラメータとして機能する。

[Author("田中太郎", Email = "tanaka@example.com", Version = "2.1")]
public class UserService
{
    [Author("鈴木花子")]
    public void CreateUser() { }
}
コンストラクタ引数

必須パラメータとして扱われる。属性を付ける際に必ず指定しなければならない。

名前付きプロパティ

省略可能なパラメータとして扱われる。指定しなければデフォルト値が使われる。

リフレクションで属性を読み取る

付与した属性はリフレクションで取得できる。GetCustomAttributes メソッドや GetCustomAttribute<T> メソッドを使う。

using System;
using System.Reflection;

var type = typeof(UserService);
var attrs = type.GetCustomAttributes<AuthorAttribute>();

foreach (var attr in attrs)
{
    Console.WriteLine($"作成者: {attr.Name}");
    if (!string.IsNullOrEmpty(attr.Email))
    {
        Console.WriteLine($"連絡先: {attr.Email}");
    }
}

メソッドに付けた属性も同様に取得できる。

var method = typeof(UserService).GetMethod("CreateUser");
var methodAttr = method?.GetCustomAttribute<AuthorAttribute>();

if (methodAttr != null)
{
    Console.WriteLine($"メソッド作成者: {methodAttr.Name}");
}

実用的なカスタム属性の例

バリデーション用のカスタム属性を作ってみよう。プロパティに付けて、値の範囲を検証する。

[AttributeUsage(AttributeTargets.Property)]
public class RangeValidationAttribute : Attribute
{
    public int Min { get; }
    public int Max { get; }

    public RangeValidationAttribute(int min, int max)
    {
        Min = min;
        Max = max;
    }

    public bool IsValid(int value) => value >= Min && value <= Max;
}

public class Student
{
    public string Name { get; set; } = "";

    [RangeValidation(0, 100)]
    public int Score { get; set; }
}

リフレクションと組み合わせて汎用的なバリデーションを実装できる。

public static bool Validate(object obj)
{
    var properties = obj.GetType().GetProperties();
    foreach (var prop in properties)
    {
        var attr = prop.GetCustomAttribute<RangeValidationAttribute>();
        if (attr != null && prop.PropertyType == typeof(int))
        {
            int value = (int)prop.GetValue(obj)!;
            if (!attr.IsValid(value))
            {
                Console.WriteLine(
                    $"{prop.Name} の値 {value}{attr.Min}{attr.Max} の範囲外です");
                return false;
            }
        }
    }
    return true;
}

カスタム属性は宣言的にメタデータを付与し、リフレクションで読み取って処理するという設計パターンの基盤になっている。ASP.NET のルーティング属性やバリデーション属性、Entity Framework のマッピング属性など、多くのフレームワークがこの仕組みの上に構築されている。自前のフレームワークや規約を作る際にも、カスタム属性は強力な武器となるだろう。