C#属性とは?Attributeの基本・使い方・自作方法を初心者向けに解説

はじめに

C#のコードを読んでいると、クラスやメソッドの直前に [Obsolete][Serializable][Required] など、角括弧で囲まれた記述を見かけることがあります。これらはC#の「属性(Attribute)」です。

属性は、クラスやメソッドなどに追加情報を与えるための仕組みです。属性を利用すると、コンパイラに警告を出させたり、入力値の検証ルールを定義したり、フレームワークにルーティング情報を伝えたりできます。

ただし、属性はコードに付けるだけで必ず処理が実行されるものではありません。属性を読み取り、その情報に基づいて動作するコンパイラ、ランタイム、フレームワーク、または自作プログラムが必要です。

この記事では、C#属性の基本的な意味と書き方、よく使う標準属性、Reflectionによる取得方法、自作属性の作成方法まで、サンプルコードを交えて解説します。

1. C#の属性(Attribute)とは?

1-1. 属性はクラス・メソッド・プロパティなどに付けるメタデータ

C#の属性とは、クラス、メソッド、プロパティ、フィールド、引数などに付加できるメタデータです。

メタデータとは、プログラムそのものの処理ではなく、プログラムを説明するための追加情報を意味します。たとえば、次のコードでは OldMethod メソッドに「このメソッドは非推奨である」という情報を付けています。

[Obsolete("NewMethodを使用してください。")]public void OldMethod(){Console.WriteLine("古いメソッドです。");}

[Obsolete] が属性です。この属性が付いたメソッドを呼び出すと、コンパイラが警告を表示します。

属性は、対象となるコードの宣言部分に記述します。一般的には、対象の直前に角括弧を使って記述します。

1-2. 属性を使うと何ができるのか

C#の属性を使うと、コードにさまざまな意味や設定を追加できます。

代表的な用途には、次のようなものがあります。

  • 非推奨のクラスやメソッドをコンパイラに知らせる

  • 入力値の必須チェックや文字数制限を定義する

  • Web APIのURLやHTTPメソッドを設定する

  • テスト対象のメソッドを指定する

  • オブジェクトのシリアライズ方法を指定する

  • ログや監査に必要な情報を付加する

  • Reflectionを使って実行時に独自情報を取得する

属性を利用すると、設定内容を対象のクラスやメソッドの近くに記述できます。そのため、設定ファイルを別に管理する方法と比べて、コードとの対応関係が分かりやすくなる場合があります。

1-3. C#の「属性」と「プロパティ」の違い

C#では、Attributeが「属性」と訳される一方、クラスのPropertyも文脈によって「属性」と呼ばれることがあります。しかし、C#のAttributeとPropertyは別の仕組みです。

プロパティは、オブジェクトが保持する値を読み書きするためのメンバーです。

public class User{public string Name { get; set; } = string.Empty;}

このコードの Name がプロパティです。

一方、属性はクラスやプロパティなどに追加情報を付ける仕組みです。

public class User{[Required]public string Name { get; set; } = string.Empty;}

このコードでは、Name がプロパティ、[Required] が属性です。

両者を整理すると、プロパティは「オブジェクトが持つデータや操作」を表し、属性は「コード要素に関する追加情報」を表します。

1-4. Attributeが使われる代表的な場面

Attributeは、C#や.NETを利用するさまざまな場面で使われています。

ASP.NET Coreでは、コントローラーやアクションに属性を付けて、ルーティングやHTTPメソッドを指定します。

[HttpGet][Route("api/users")]public IActionResult GetUsers(){return Ok();}

入力モデルでは、DataAnnotations系の属性を使って検証ルールを設定できます。

public class UserInput{[Required][StringLength(50)]public string Name { get; set; } = string.Empty;}

テストフレームワークでは、属性によってテストメソッドを識別します。たとえば、xUnitでは [Fact]、NUnitでは [Test]、MSTestでは [TestMethod] などが使われます。

このように、属性はコンパイラだけでなく、フレームワークやライブラリに設定情報を伝える手段として広く利用されています。

2. C#属性の基本的な書き方

2-1. 属性の基本構文

C#属性の基本構文は次のとおりです。

[属性名]対象の宣言

属性に値を渡す場合は、丸括弧の中に引数を記述します。

[属性名("値")]対象の宣言

複数の情報を設定する場合は、コンストラクタ引数や名前付き引数を利用します。

[属性名("必須の値", Option = true)]対象の宣言

属性は通常、対象の宣言の直前に記述します。

2-2. クラスに属性を付ける方法

クラスに属性を付ける場合は、クラス宣言の直前に記述します。

[Serializable]public class User{public string Name { get; set; } = string.Empty;}

この例では、User クラスに Serializable 属性を付けています。

属性によっては、クラス以外の構造体や列挙型、インターフェース、デリゲートなどにも付けられます。ただし、どの対象に付けられるかは属性ごとに決められています。

2-3. メソッドに属性を付ける方法

メソッドに属性を付ける場合も、メソッド宣言の直前に記述します。

[Obsolete("新しいメソッドを使用してください。")]public void OldMethod(){Console.WriteLine("古い処理");}

この属性は、メソッドの処理内容を直接変更するものではありません。コンパイラが属性を認識し、そのメソッドを使用しているコードに警告を表示します。

2-4. プロパティ・フィールド・引数に属性を付ける方法

属性はプロパティにも付けられます。

public class User{[Required]public string Name { get; set; } = string.Empty;}

フィールドに付ける例は次のとおりです。

public class Sample{[NonSerialized]private string? temporaryData;}

メソッドの引数に付けることもできます。

public void Register([Required] string name){Console.WriteLine(name);}

戻り値に属性を付けたい場合は、return: を指定します。

[return: NotNull]public string? GetName(){return "Taro";}

属性を付ける対象が曖昧になる場合は、field:property:method:return: などの属性ターゲットを明示できます。

[field: NonSerialized]public string Cache { get; set; } = string.Empty;

2-5. 複数の属性を付ける方法

同じ対象に複数の属性を付ける場合は、属性を複数行に分けて記述できます。

[Required][StringLength(50)]public string Name { get; set; } = string.Empty;

1組の角括弧の中に、カンマ区切りで記述することも可能です。

[Required, StringLength(50)]public string Name { get; set; } = string.Empty;

一般的には、属性ごとの設定内容を確認しやすい複数行の書き方がよく使われます。

同じ種類の属性を複数付けられるかどうかは、その属性に設定された AttributeUsageAllowMultiple によって決まります。

2-6. 属性名の「Attribute」は省略できる

C#の属性クラスは、通常、クラス名の末尾に Attribute を付けます。

たとえば、Obsolete 属性の正式なクラス名は ObsoleteAttribute です。

次の2つの記述は同じ意味です。

[ObsoleteAttribute]public void OldMethod(){}
[Obsolete]public void OldMethod(){}

C#では、属性として使用するときに限り、末尾の Attribute を省略できます。

自作属性を作る場合も、クラス名を SampleAttribute のようにしておけば、使用時には [Sample] と記述できます。

3. C#でよく使う標準属性

3-1. Obsolete属性:非推奨メンバーを知らせる

Obsolete 属性は、古くなったクラスやメソッド、プロパティなどを非推奨として知らせるための属性です。

[Obsolete("OldMethodは非推奨です。NewMethodを使用してください。")]public void OldMethod(){}

このメソッドを呼び出すと、コンパイラが警告を表示します。

第2引数に true を指定すると、警告ではなくコンパイルエラーにできます。

[Obsolete("このメソッドは使用できません。", true)]public void RemovedMethod(){}

既存APIをすぐに削除できない場合に、利用者へ移行を促す手段として有効です。

3-2. Serializable属性:シリアライズ対象にする

Serializable 属性は、型がシリアライズ可能であることを示す属性です。

[Serializable]public class User{public string Name { get; set; } = string.Empty;}

シリアライズとは、オブジェクトの状態を保存や送信が可能な形式に変換する処理です。

ただし、Serializable 属性が必要かどうかは、使用するシリアライザーによって異なります。たとえば、System.Text.Json による一般的なJSONシリアライズでは、通常 [Serializable] を付ける必要はありません。

そのため、「シリアライズするクラスには必ず [Serializable] を付ける」と覚えるのではなく、利用するフレームワークやライブラリの仕様を確認することが重要です。

3-3. Conditional属性:条件付きでメソッドを呼び出す

Conditional 属性は、指定したコンパイルシンボルが定義されている場合だけ、メソッド呼び出しを有効にする属性です。

using System.Diagnostics;

public class Logger{[Conditional("DEBUG")]public static void DebugLog(string message){Console.WriteLine(message);}}

呼び出し側は通常のメソッドと同じように記述します。

Logger.DebugLog("デバッグ情報");

DEBUG シンボルが定義されていないビルドでは、対象メソッドの呼び出し自体がコンパイル結果から省かれます。

Conditional 属性を付けるメソッドには、戻り値を void にするなどの制約があります。デバッグ用ログや診断用処理で利用される属性です。

3-4. AttributeUsage属性:属性を付けられる対象を制限する

AttributeUsage 属性は、自作属性をどのコード要素に付けられるか指定するために使います。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]public class AuditAttribute : Attribute{}

この例の Audit 属性は、クラスまたはメソッドに付けられます。

プロパティに付けようとするとコンパイルエラーになります。

[Audit]public class OrderService{[Audit]public void CreateOrder(){}}

AttributeUsage では、同じ属性を複数付けられるか、派生クラスに継承されるかも指定できます。

[AttributeUsage(AttributeTargets.Method,AllowMultiple = true,Inherited = true)]public class RoleAttribute : Attribute{}

3-5. DataAnnotations系属性:入力チェックやモデル定義で使う

System.ComponentModel.DataAnnotations 名前空間には、入力チェックやモデル定義に利用できる属性が用意されています。

using System.ComponentModel.DataAnnotations;

public class UserInput{[Required][StringLength(50, MinimumLength = 2)]public string Name { get; set; } = string.Empty;

<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[EmailAddress]</span>public string Email { get; set; } = string.Empty;<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Range(0, 120)]</span>public int Age { get; set; }

}

代表的な属性には、次のようなものがあります。

  • Required:値を必須にする

  • StringLength:文字列の長さを制限する

  • Range:数値の範囲を制限する

  • EmailAddress:メールアドレス形式を検証する

  • RegularExpression:正規表現による検証を行う

  • Display:画面表示用の名前を設定する

ASP.NET CoreやEntity Framework Coreなどでは、これらの属性がモデルの検証や定義に利用されます。ただし、実際の動作は属性を読み取るフレームワーク側の仕組みに依存します。

4. C#属性の使い方をサンプルコードで理解する

4-1. Obsolete属性を使った基本例

次のクラスには、古いメソッドと新しいメソッドがあります。

public class Calculator{[Obsolete("AddNewメソッドを使用してください。")]public int AddOld(int x, int y){return x + y;}

public int AddNew(int x, int y){return x + y;}

}

AddOld を呼び出すコードを書くと、コンパイラが警告を出します。

var calculator = new Calculator();

int result = calculator.AddOld(10, 20);Console.WriteLine(result);

メソッド自体は実行できますが、開発者は警告によって非推奨であることを把握できます。

既存コードとの互換性を残しながら、新しいAPIへ段階的に移行してもらいたい場合に便利です。

4-2. 属性に引数を渡す方法

属性には、コンストラクタを通じて値を渡せます。

[Obsolete("この機能は次のバージョンで削除されます。")]public void LegacyFeature(){}

文字列 "この機能は次のバージョンで削除されます。" が、ObsoleteAttribute のコンストラクタに渡されています。

自作属性でも同じように、コンストラクタを定義すれば必須情報を受け取れます。

public class DescriptionAttribute : Attribute{public string Text { get; }

public DescriptionAttribute(string text){Text = text;}

}

使用時は次のように記述します。

[Description("ユーザーを登録する処理")]public void RegisterUser(){}

4-3. 名前付き引数を使う方法

属性の書き込み可能なプロパティやフィールドには、名前付き引数で値を設定できます。

public class DescriptionAttribute : Attribute{public string Text { get; }

public string Category { get; set; } = string.Empty;public DescriptionAttribute(string text){Text = text;}

}

属性を使用するコードは次のとおりです。

[Description("ユーザーを登録する処理",Category = "User")]public void RegisterUser(){}

"ユーザーを登録する処理" はコンストラクタ引数、Category = "User" は名前付き引数です。

コンストラクタ引数は、属性を使うために欠かせない情報に適しています。名前付き引数は、省略可能な追加設定に適しています。

なお、属性の引数に指定できる値には制約があります。基本型、文字列、Type、列挙型、およびそれらの配列など、コンパイル時に属性情報として記録できる型を使用します。

4-4. 属性を付けたコードが実行時にどう扱われるか

属性情報は、コンパイル後のアセンブリ内にメタデータとして記録されます。

プログラムはReflectionを使うことで、実行時に次のような情報を調べられます。

  • 指定したクラスに特定の属性が付いているか

  • 属性のコンストラクタにどの値が渡されているか

  • 属性のプロパティにどの値が設定されているか

  • メソッドやプロパティにどの属性が付いているか

たとえば、クラスに付けられた属性は次のように取得できます。

Type type = typeof(UserService);

object[] attributes = type.GetCustomAttributes(inherit: true);

foreach (object attribute in attributes){Console.WriteLine(attribute.GetType().Name);}

この仕組みを利用して、フレームワークは属性によるルーティング、入力検証、テスト検出などを実現しています。

4-5. 属性は単体では処理を実行しない

属性を理解するうえで特に重要なのは、属性を付けただけでは任意の処理が自動的に実行されるわけではない点です。

次の自作属性を考えてみましょう。

public class LogAttribute : Attribute{}

メソッドに属性を付けます。

[Log]public void Save(){Console.WriteLine("保存しました。");}

この状態で Save を呼び出しても、ログ処理が自動的に追加されるわけではありません。

LogAttribute を読み取り、ログを出力するコードやフレームワークの仕組みが別途必要です。

属性は処理そのものではなく、基本的には「この対象を特別に扱ってほしい」という目印や設定情報です。

5. Reflectionで属性情報を取得する方法

5-1. Reflectionとは

Reflectionとは、実行中のプログラムから型やメンバーの情報を調べるための仕組みです。

Reflectionを使うと、次のような情報を取得できます。

  • クラス名や名前空間

  • 基底クラスや実装インターフェース

  • メソッド、プロパティ、フィールド

  • 引数や戻り値の型

  • 付与されている属性

C#では、主に System.TypeSystem.Reflection 名前空間のクラスを利用します。

Type type = typeof(User);

Console.WriteLine(type.Name);Console.WriteLine(type.FullName);

実行中のオブジェクトから型を取得する場合は、GetType を使います。

var user = new User();Type type = user.GetType();

5-2. GetCustomAttributesで属性を取得する

属性を取得する代表的なメソッドが GetCustomAttributes です。

Type type = typeof(User);

object[] attributes = type.GetCustomAttributes(inherit: true);

foreach (object attribute in attributes){Console.WriteLine(attribute.GetType().Name);}

特定の属性だけを取得したい場合は、型を指定できます。

object[] attributes = type.GetCustomAttributes(typeof(SerializableAttribute),inherit: true);

ジェネリック形式の GetCustomAttribute<T> を使うと、コードを簡潔にできます。

using System.Reflection;

DescriptionAttribute? attribute =type.GetCustomAttribute<DescriptionAttribute>();

属性が付いているかだけを調べる場合は、IsDefined も利用できます。

bool exists = Attribute.IsDefined(typeof(User),typeof(SerializableAttribute));

5-3. クラスに付いた属性を取得する

まず、自作属性を定義します。

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

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

}

クラスに属性を付けます。

[ServiceInfo("ユーザー管理サービス")]public class UserService{}

Reflectionで読み取るコードは次のとおりです。

using System.Reflection;

Type type = typeof(UserService);

ServiceInfoAttribute? attribute =type.GetCustomAttribute<ServiceInfoAttribute>();

if (attribute is not null){Console.WriteLine(attribute.Name);}

実行結果は次のようになります。

ユーザー管理サービス

5-4. メソッドやプロパティに付いた属性を取得する

メソッドの属性を取得するには、最初に MethodInfo を取得します。

using System.Reflection;

MethodInfo? method =typeof(UserService).GetMethod("Register");

if (method is not null){DescriptionAttribute? attribute =method.GetCustomAttribute<DescriptionAttribute>();

if (attribute is not null){Console.WriteLine(attribute.Text);}

}

プロパティの場合は PropertyInfo を利用します。

PropertyInfo? property =typeof(User).GetProperty("Name");

if (property is not null){RequiredAttribute? attribute =property.GetCustomAttribute<RequiredAttribute>();

Console.WriteLine(attribute is not null? "必須項目です。": "任意項目です。");

}

すべてのプロパティを調べることもできます。

PropertyInfo[] properties =typeof(User).GetProperties();

foreach (PropertyInfo property in properties){bool isRequired =property.IsDefined(typeof(RequiredAttribute),inherit: true);

Console.WriteLine($"{property.Name}: Required={isRequired}");

}

5-5. 属性情報を実務で活用する例

Reflectionによる属性取得は、次のような機能に活用できます。

  • 必須項目を自動的に検証する

  • 属性が付いたメソッドだけをログ対象にする

  • メニュー項目や画面表示名を自動生成する

  • 権限属性からアクセス可能な処理を判定する

  • CSV出力時の列名や順序を指定する

  • コマンド名と実行メソッドを対応付ける

  • プラグインとして読み込むクラスを識別する

たとえば、CSVの列名を属性で定義できます。

[AttributeUsage(AttributeTargets.Property)]public class CsvColumnAttribute : Attribute{public string Header { get; }

public int Order { get; set; }public CsvColumnAttribute(string header){Header = header;}

}

モデル側では次のように指定します。

public class User{[CsvColumn("ユーザーID", Order = 1)]public int Id { get; set; }

<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[CsvColumn("氏名", Order = 2)]</span>public string Name { get; set; } = string.Empty;

}

CSV出力処理がReflectionで属性を読み取れば、モデルのプロパティとCSV列の対応を宣言的に管理できます。

6. C#で自作属性を作る方法

6-1. 自作属性はAttributeクラスを継承して作る

C#で独自の属性を作る場合は、System.Attribute クラスを継承します。

public class SampleAttribute : Attribute{}

クラス名の末尾には、慣例として Attribute を付けます。

使用するときは、末尾を省略して [Sample] と記述できます。

[Sample]public class SampleService{}

自作属性を作るだけでは、対象クラスの動作は変わりません。属性を読み取って利用する処理も設計する必要があります。

6-2. 自作属性の基本コード

メソッドの説明を保持する属性を作ってみましょう。

[AttributeUsage(AttributeTargets.Method)]public class OperationAttribute : Attribute{public string Description { get; }

public OperationAttribute(string description){Description = description;}

}

メソッドに属性を付けます。

public class OrderService{[Operation("注文を登録します。")]public void CreateOrder(){Console.WriteLine("注文登録");}}

これで、CreateOrder メソッドに説明情報を付加できました。

6-3. コンストラクタで値を受け取る

属性を使用する際に必ず指定してほしい情報は、コンストラクタで受け取ります。

public class OperationAttribute : Attribute{public string Code { get; }

public string Description { get; }public OperationAttribute(string code,string description){Code = code;Description = description;}

}

使用例は次のとおりです。

[Operation("ORDER_CREATE", "注文を登録します。")]public void CreateOrder(){}

この場合、CodeDescription は必須です。どちらかを省略するとコンパイルできません。

6-4. プロパティで追加情報を持たせる

必須ではない設定は、書き込み可能なプロパティとして定義すると便利です。

public class OperationAttribute : Attribute{public string Code { get; }

public string Description { get; set; } = string.Empty;public bool RequiresAdmin { get; set; }public OperationAttribute(string code){Code = code;}

}

使用時は名前付き引数で設定します。

[Operation("ORDER_DELETE",Description = "注文を削除します。",RequiresAdmin = true)]public void DeleteOrder(){}

Code は必須、DescriptionRequiresAdmin は任意です。

このように、必須値をコンストラクタ、任意値をプロパティに分けると、属性の使い方が分かりやすくなります。

6-5. AttributeUsageで使用対象を制限する

自作属性には、できるだけ AttributeUsage を付けて使用対象を明確にしましょう。

[AttributeUsage(AttributeTargets.Method,AllowMultiple = false,Inherited = true)]public class OperationAttribute : Attribute{public string Code { get; }

public OperationAttribute(string code){Code = code;}

}

主な設定項目は次のとおりです。

AttributeTargets は、属性を付けられる対象を指定します。

AttributeTargets.ClassAttributeTargets.MethodAttributeTargets.PropertyAttributeTargets.FieldAttributeTargets.ParameterAttributeTargets.ConstructorAttributeTargets.InterfaceAttributeTargets.All

複数の対象を許可する場合は、| で組み合わせます。

[AttributeUsage(AttributeTargets.Class |AttributeTargets.Method)]

AllowMultiple は、同じ対象に同じ属性を複数付けられるかを指定します。

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

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

}

使用例は次のとおりです。

[Role("Admin")][Role("Manager")]public void Approve(){}

Inherited は、派生クラスやオーバーライドされたメンバーを調べる際の継承に関する設定です。ただし、実際に継承属性が返されるかどうかは、対象となるメンバーや使用するReflection APIにも左右されます。

6-6. 自作属性をReflectionで読み取る

自作属性をメソッドから取得してみましょう。

using System.Reflection;

Type type = typeof(OrderService);

foreach (MethodInfo method in type.GetMethods()){OperationAttribute? attribute =method.GetCustomAttribute<OperationAttribute>();

if (attribute is null){continue;}Console.WriteLine($"メソッド: {method.Name}");Console.WriteLine($"コード: {attribute.Code}");Console.WriteLine($"説明: {attribute.Description}");Console.WriteLine($"管理者限定: {attribute.RequiresAdmin}");

}

属性が複数付けられる場合は、GetCustomAttributes<T> を使います。

IEnumerable<RoleAttribute> roles =method.GetCustomAttributes<RoleAttribute>();

foreach (RoleAttribute role in roles){Console.WriteLine(role.Name);}

取得した情報に基づいて、アクセス制御、ログ出力、画面生成などの処理を実装できます。

7. C#属性の実践的な活用例

7-1. 入力バリデーションで使う

入力バリデーションは、C#属性の代表的な用途です。

using System.ComponentModel.DataAnnotations;

public class RegisterUserRequest{[Required(ErrorMessage = "名前は必須です。")][StringLength(50,MinimumLength = 2,ErrorMessage = "名前は2~50文字で入力してください。")]public string Name { get; set; } = string.Empty;

<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Required]</span><span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[EmailAddress(ErrorMessage = "メールアドレスの形式が不正です。")]</span>public string Email { get; set; } = string.Empty;<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Range(18,120,ErrorMessage = "年齢は18~120の範囲で入力してください。")]</span>public int Age { get; set; }

}

DataAnnotationsの検証機能を直接使う場合は、次のように記述できます。

var request = new RegisterUserRequest{Name = "",Email = "invalid",Age = 10};

var context = new ValidationContext(request);var results = new List<ValidationResult>();

bool isValid = Validator.TryValidateObject(request,context,results,validateAllProperties: true);

foreach (ValidationResult result in results){Console.WriteLine(result.ErrorMessage);}

ASP.NET Coreでは、モデルバインディングや検証処理と組み合わせてDataAnnotations属性が利用されます。

7-2. ログ出力や監査情報に使う

特定の処理を監査対象として識別するために、自作属性を利用できます。

[AttributeUsage(AttributeTargets.Method)]public class AuditAttribute : Attribute{public string ActionName { get; }

public AuditAttribute(string actionName){ActionName = actionName;}

}

使用例は次のとおりです。

public class AccountService{[Audit("ユーザー削除")]public void DeleteUser(int userId){Console.WriteLine($"ユーザー {userId} を削除しました。");}}

実際に監査ログを出力するには、属性をReflectionで読み取る共通処理、プロキシ、フィルター、インターセプターなどが必要です。

属性を付けただけでメソッドの前後にログが自動追加されるわけではない点に注意してください。

7-3. APIやルーティング設定で使う

ASP.NET Coreでは、属性を使ってAPIのルーティングやHTTPメソッドを設定できます。

[ApiController][Route("api/[controller]")]public class UsersController : ControllerBase{[HttpGet("{id:int}")]public IActionResult Get(int id){return Ok(new{Id = id,Name = "Taro"});}

<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[HttpPost]</span>public IActionResult Create(<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[FromBody]</span> RegisterUserRequest request){return Ok(request);}

}

この例では、次の属性が使われています。

  • ApiController:APIコントローラーとしての動作を有効にする

  • Route:基本となるURLを指定する

  • HttpGet:GETリクエストを処理する

  • HttpPost:POSTリクエストを処理する

  • FromBody:リクエスト本文から値を取得する

フレームワークがこれらの属性を読み取り、リクエストとメソッドを対応付けます。

7-4. テストコードで使う

C#のテストフレームワークでは、テスト対象の識別やテスト条件の設定に属性が使われます。

xUnitの例は次のとおりです。

public class CalculatorTests{[Fact]public void Add_2と3を渡すと5になる(){var calculator = new Calculator();

    int result = calculator.AddNew(2, 3);Assert.Equal(5, result);}<span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[Theory]</span><span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[InlineData(1, 2, 3)]</span><span data-placeholder-token="true" class="text-token-text-primary cursor-text rounded-sm" style="background-color: color-mix(in srgb, var(--theme-user-selection-bg, var(--selection)) 30%, transparent); padding-top: 4px; padding-bottom: 4px;">[InlineData(10, 20, 30)]</span>public void Add_値を加算できる(int x,int y,int expected){var calculator = new Calculator();int result = calculator.AddNew(x, y);Assert.Equal(expected, result);}

}

Fact は単一のテスト、TheoryInlineData は複数の入力値を使用するデータ駆動テストを表します。

テストランナーはアセンブリ内の属性を調べ、実行すべきテストメソッドを検出します。

7-5. フレームワークやライブラリで属性が使われる理由

フレームワークやライブラリで属性が多く使われる理由は、設定を宣言的に記述できるからです。

通常の命令的なコードでは、「どの条件で、どの処理を、どの順番で実行するか」を細かく記述します。

一方、属性を使う方法では、「このメソッドはGETリクエスト用」「このプロパティは必須」「このメソッドはテスト対象」といった意図を宣言します。具体的な処理はフレームワーク側が担当します。

属性を使う主な利点は次のとおりです。

  • 設定と対象コードを近くに記述できる

  • 同じ仕組みを複数のクラスに適用しやすい

  • 定型的な処理をフレームワークに任せられる

  • クラスやメソッドを自動検出しやすい

  • 設定内容をコードとして型安全に扱いやすい

一方で、処理の流れがコードから見えにくくなることもあるため、過剰な使用には注意が必要です。

8. C#属性を使うときの注意点

8-1. 属性を付けただけでは処理は変わらない

自作属性を付けただけでは、対象のメソッドやクラスの処理は変化しません。

[Audit("データ更新")]public void Update(){}

このコードに Audit 属性を付けても、監査ログを出力する処理が存在しなければ何も起こりません。

属性を利用する際は、誰が属性を読み取るのかを明確にしましょう。

  • コンパイラが読み取る

  • .NETランタイムが読み取る

  • ASP.NET Coreなどのフレームワークが読み取る

  • テストランナーが読み取る

  • 自作したReflection処理が読み取る

属性の付与と、それを解釈する仕組みはセットで考える必要があります。

8-2. Reflectionの使いすぎに注意する

Reflectionは柔軟な仕組みですが、通常のメソッド呼び出しやプロパティアクセスよりも処理が複雑になりやすく、実行コストも増える可能性があります。

大量のデータを処理するループ内で、毎回同じ属性を取得するような実装は避けたほうがよいでしょう。

foreach (var item in items){// 毎回同じ型情報を取得する設計は避けるvar attributes =item.GetType().GetCustomAttributes(true);}

同じ属性情報を繰り返し利用する場合は、最初に取得してキャッシュする方法が考えられます。

private static readonly Dictionary<Type,PropertyInfo[]> Cache = new();

また、目的によってはインターフェース、基底クラス、ジェネリック型、ソースジェネレーターなど、Reflection以外の方法が適している場合もあります。

8-3. 属性に複雑なロジックを持たせない

属性クラスは、基本的にメタデータを保持するためのクラスです。属性のコンストラクタやプロパティに、データベースアクセスや外部API呼び出しなどの複雑な処理を持たせるべきではありません。

避けたい設計の例は次のとおりです。

public class BadAttribute : Attribute{public BadAttribute(){// データベース接続などの重い処理}}

属性には識別子、表示名、数値、フラグなどの設定情報を持たせ、実際の処理はサービスクラスやフレームワーク側に分離しましょう。

8-4. 使いどころを誤るとコードが分かりにくくなる

属性はコードを簡潔にできますが、使いすぎると実際の動作が見えにくくなります。

[Authorize][Audit][Transaction][Retry][Cache][Validate]public void Execute(){}

このように多くの属性が付くと、メソッドを呼び出した際に何が起きるのか理解しにくくなります。

属性が適しているのは、主に宣言的な設定や横断的な関心事です。業務処理の重要な分岐や複雑なロジックまで属性に隠すと、デバッグや保守が難しくなる可能性があります。

処理の流れを明示したほうが分かりやすい場合は、通常のメソッド呼び出しや依存性注入を利用しましょう。

8-5. 命名ルールと可読性を意識する

自作属性のクラス名には、末尾に Attribute を付けるのが一般的です。

public class AuditAttribute : Attribute{}

使用時には次のように省略できます。

[Audit]

属性名は、目的が伝わる具体的な名前にしましょう。

[RequiresPermission("Order.Delete")][CsvColumn("商品名")][RetryCount(3)]

InfoAttributeCustomAttribute のような曖昧な名前では、何のための属性か判断しにくくなります。

また、コンストラクタ引数が多すぎる属性も可読性を下げます。必須項目を絞り、任意設定は名前付き引数に分けると理解しやすくなります。

9. C#属性に関するよくある質問

9-1. C#の属性とアノテーションは同じもの?

概念としてはよく似ています。

Javaでは「アノテーション」、C#では「属性(Attribute)」という名称が使われます。どちらも、クラスやメソッドなどにメタデータを付加する仕組みです。

ただし、構文、継承方法、指定できる値、実行時の取得方法など、具体的な仕様は異なります。

Javaのアノテーションを使った経験がある人は、C#のAttributeも「コードに追加情報を付ける仕組み」と考えると理解しやすいでしょう。

9-2. 属性とコメントの違いは?

コメントは、人間がコードを理解するための文章です。

// このメソッドは非推奨ですpublic void OldMethod(){}

通常、コメントをコンパイラやプログラムが設定情報として利用することはありません。

一方、属性はコンパイル後のメタデータに記録され、コンパイラ、ランタイム、フレームワーク、Reflectionなどから利用できます。

[Obsolete("NewMethodを使用してください。")]public void OldMethod(){}

コメントは説明のため、属性は機械的に解釈できる追加情報のために使うのが基本です。

9-3. 属性は実行時に必ず取得できる?

一般的なC#の属性は、コンパイル後のメタデータに保存され、Reflectionから取得できます。

ただし、実際にどのように取得できるかは、次の条件によって変わることがあります。

  • 属性を付けた対象

  • AttributeUsageInherited 設定

  • 使用するReflection API

  • 継承元と派生先の関係

  • ビルドやコード削減の設定

  • 属性インスタンスを生成するAPIか、メタデータだけを参照するAPIか

属性のコンストラクタを実行せずにメタデータを確認したい場合は、CustomAttributeData を使う方法もあります。

IList<CustomAttributeData> attributes =CustomAttributeData.GetCustomAttributes(typeof(UserService));

foreach (CustomAttributeData attribute in attributes){Console.WriteLine(attribute.AttributeType.Name);}

実行環境や配布方式によってはReflection対象が削減される場合もあるため、特にトリミングやAhead-of-Timeコンパイルを利用するアプリでは注意が必要です。

9-4. 自作属性はどんな場面で使うべき?

自作属性は、複数のクラスやメソッドに共通する宣言的な情報を付けたい場合に適しています。

たとえば、次のような場面です。

  • CSVやExcelの列名を指定する

  • 操作コードや監査対象を指定する

  • 権限やロールの情報を付ける

  • プラグインとして読み込むクラスを識別する

  • コマンド名と実行メソッドを対応付ける

  • 画面表示名や並び順を設定する

  • 自作フレームワークの処理対象を示す

反対に、単純な条件分岐や1か所でしか使わない設定のために属性を作ると、仕組みが複雑になる可能性があります。

自作属性を導入する前に、インターフェース、設定クラス、列挙型、通常のプロパティなどで、より単純に実現できないか検討しましょう。

9-5. 初心者はまずどの属性から覚えるべき?

初心者は、用途が分かりやすく、実際の開発で目にする機会が多い属性から覚えるのがおすすめです。

最初は、次の属性を確認するとよいでしょう。

  • Obsolete:非推奨であることを知らせる

  • Required:入力を必須にする

  • StringLength:文字列の長さを制限する

  • Range:数値の範囲を制限する

  • Conditional:条件付きでメソッド呼び出しを有効にする

  • AttributeUsage:自作属性の使用対象を定義する

ASP.NET Coreを学ぶ場合は、RouteHttpGetHttpPostFromBody なども重要です。

テストを学ぶ場合は、利用するテストフレームワークの FactTheoryTestTestMethod などを確認しましょう。

標準属性の使い方を理解した後に、Attribute クラスの継承とReflectionによる取得へ進むと、属性の仕組みを段階的に学べます。

まとめ

C#の属性(Attribute)は、クラス、メソッド、プロパティ、フィールド、引数などにメタデータを付加する仕組みです。

属性は角括弧を使って、対象となる宣言の直前に記述します。

[Obsolete("新しいメソッドを使用してください。")]public void OldMethod(){}

標準属性には、非推奨を知らせる Obsolete、条件付き呼び出しを指定する Conditional、入力検証に使う RequiredStringLength などがあります。

独自の属性を作る場合は、Attribute クラスを継承します。

[AttributeUsage(AttributeTargets.Method)]public class OperationAttribute : Attribute{public string Code { get; }

public OperationAttribute(string code){Code = code;}

}

付与した属性は、Reflectionの GetCustomAttributeGetCustomAttributes などを使って取得できます。

ただし、属性は単体で任意の処理を実行するものではありません。属性を読み取り、その情報に基づいて動作するコンパイラ、フレームワーク、ライブラリ、または自作処理が必要です。

まずは Obsolete やDataAnnotations系の標準属性を実際に使い、属性がコードにどのような情報を与えるのか確認してみましょう。その後、自作属性とReflectionを組み合わせることで、宣言的で再利用しやすい仕組みを実装できるようになります。