こんにちは、アーティサン株式会社の戸田隆俊と申します。
私は主に C#でのバックエンド開発を担当しております。
コーディングをしていく中でバリデーションというのは何度も同じような処理を書く必要があり、これをどうにか出来ないかという思いから本記事を執筆致しました。
アトリビュートを用いることでバリデーションをより簡単に、かつ保守性・可読性に優れたコーディングで実装する方法をご紹介します。
環境
- .Net Core 3.1
仕様
Person クラスに対してチェック処理を実装
- ID、名前は必須
- 誕生日 <= 現在日付
- 0 <= 身長 <= 1000
- 0 <= 体重 <= 1000
アトリビュートとは
アトリビュート(属性)とは、クラス・メソッド・プロパティ等に追加情報を関連付けることが出来る機能です。
リフレクションを利用してプログラム実行時にその追加情報を参照することができます。
また、アトリビュートは複数適用することができ、メソッドやプロパティと同様に引数を受け取ることができます。
[アトリビュート名]のように記載されます。
一般的なバリデーション
みなさん、バリデーションは実装していますか?
コーディングをする人で実装経験がない方はいないのではないかと言うくらい必須の処理かと思います。私自身今までは下記のような実装を繰り返していました。
サンプルコード
今回チェック対象となるクラスです。
ID、名前、誕生日、身長体重のプロパティを持ちます。
public class Person
{
public Guid? Id { get; internal set; }
public string Name { set; get; }
public DateTime? Birthday { set; get; }
public int? Height { set; get; }
public int? Weight { set; get; }
}
実際のバリデーションです。
22,25 行目で必須チェック、28 行目で誕生日が現在より大きいことをチェック、
31,34 行目で身長・体重が 0 以上 1000 以下であることをチェックしています。
class Program
{
static void Main(string[] args)
{
var p = new Person
{
Id = Guid.NewGuid(),
Name = "テスト太郎",
Birthday = DateTime.Now.Date,
Height = 180,
Weight = 70,
};
var errMsg = Validation(p);
Console.Write(errMsg);
}
static string Validation(Person p)
{
var msg = new StringBuilder();
if (p.Id == null)
msg.AppendLine($"{nameof(p.Id)}:error");
if (p.Name == null)
msg.AppendLine($"{nameof(p.Name)}:error");
if (p.Birthday > DateTime.Now.Date)
msg.AppendLine($"{nameof(p.Birthday)}:error");
if (p.Height < 0 || p.Height > 1000)
msg.AppendLine($"{nameof(p.Height)}:error");
if (p.Weight < 0 || p.Weight > 1000)
msg.AppendLine($"{nameof(p.Weight)}:error");
return msg.ToString();
}
}
問題点
こちらは正常に動作しますが、以下のような問題があります。
- バリデーション対象が増えた場合にそれに応じた処理の実装が必要
- 仕様が変更された場合に各処理を修正する必要がある
実際のプロジェクトではクラスは複数の個所で使用されていると思いますので、全ての個所に処理を実装・修正する必要があります。
アトリビュートを用いたバリデーション
バリデーションにアトリビュートを用いることでこういった問題点が解消され、保守性・可読性にすぐれたコーディングができます。
下記が今回ご紹介したいコードとなります。
サンプルコード
アトリビュートクラスです。
DateUnderの引数に関しては DateTime が使えない為stringで代用しています。
また、引数が指定されていない場合はシステム日付を設定します。
[System.AttributeUsage(System.AttributeTargets.Property)]
public class Required : Attribute { }
[System.AttributeUsage(System.AttributeTargets.Property)]
public class DateUnder : Attribute
{
public DateTime UnderDay { get; set; }
public DateUnder(string day = null)
{
UnderDay = day == null ? DateTime.Now : DateTime.Parse(day);
}
}
[System.AttributeUsage(System.AttributeTargets.Property)]
public class IntBetween : Attribute
{
public int OrUnderVal { get; set; }
public int AndOverVal { get; set; }
public IntBetween(int val1, int val2)
{
OrUnderVal = val1;
AndOverVal = val2;
}
}
チェック対象となるクラスです。
各プロパティに対してアトリビュートを設定しています。
アトリビュートを分かりやすい名前にすることによって、保守性・可読性が向上します。
public class Person
{
[Required]
public Guid? Id { get; internal set; }
[Required]
public string Name { set; get; }
[DateUnder]
public DateTime? Birthday { set; get; }
[IntBetween(0, 1000)]
public int? Height { set; get; }
[IntBetween(0, 1000)]
public int? Weight { set; get; }
}
バリデーションの拡張メソッドです。
プロパティからアトリビュートを判定して各バリデーションを行っています。
今回はエラーメッセージを返す作りにしています。
public static class ValidateExt
{
public static string Validation<T>(this T obj) where T : class
{
var msg = new StringBuilder();
var props = typeof(T).GetProperties();
foreach (var prop in props)
{
foreach (var attr in prop.GetCustomAttributes())
{
switch (attr)
{
case Required at:
if (prop.GetValue(obj) == null)
msg.AppendLine($"{prop.Name}:error");
break;
case DateUnder at:
if (prop.GetValue(obj) as DateTime? >= at.UnderDay)
msg.AppendLine($"{prop.Name}:error");
break;
case IntBetween at:
if (prop.GetValue(obj) as int? < at.OrUnderVal || prop.GetValue(obj) as int? > at.AndOverVal)
msg.AppendLine($"{prop.Name}:error");
break;
}
}
}
return msg.ToString();
}
}
以下が実際のアトリビュートを用いたバリデーションです。
14行目で拡張メソッドを呼び出してバリデーションを行っています。
class Program
{
static void Main(string[] args)
{
var p = new Person
{
Id = Guid.NewGuid(),
Name = "テスト太郎",
Birthday = DateTime.Now.Date,
Height = 180,
Weight = 70,
};
var errMsg = p.Validation();
Console.Write(errMsg);
}
}
あとがき
いかがでしたでしょうか?
このようにアトリビュートを用いることでバリデーションを一つの拡張メソッドにまとめることができます。
また、アトリビュートの性質上クラスを見ればどのようなバリデーションが実装されているか一目で分かり、汎用的なバリデーションを作成することで様々なプロジェクトで転用できるかと思います。
これにより保守性・可動性に優れたコーディングが可能となります。
ぜひ、一度お試し下さい。
戸田隆俊
SE歴は約9年間!2021年3月にアーティサンに入社し、主にC#のバックエンド開発を担当しています。
新しいもの好きで新機能はどんどん使っていきたいタイプです。
かゆいところに手が届くような記事を作っていければと思います。
ちなみに趣味は映画鑑賞(ファンタジー系)でアマプラにドはまりしてます!