技術情報ブログ
c#
2021.07.21

C# アトリビュートを用いたバリデーションでコーディングの保守性・可読性の向上を実現する

C# アトリビュートを用いたバリデーションでコーディングの保守性・可読性の向上を実現する

こんにちは、アーティサン株式会社の戸田隆俊と申します。

 

私は主に 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);
    }
}

 

あとがき

いかがでしたでしょうか?

このようにアトリビュートを用いることでバリデーションを一つの拡張メソッドにまとめることができます。

また、アトリビュートの性質上クラスを見ればどのようなバリデーションが実装されているか一目で分かり、汎用的なバリデーションを作成することで様々なプロジェクトで転用できるかと思います。

これにより保守性・可動性に優れたコーディングが可能となります。

 

ぜひ、一度お試し下さい。

C#のバックエンド開発を担当:戸田隆俊

戸田隆俊

SE歴は約9年間!2021年3月にアーティサンに入社し、主にC#のバックエンド開発を担当しています。

新しいもの好きで新機能はどんどん使っていきたいタイプです。

かゆいところに手が届くような記事を作っていければと思います。

ちなみに趣味は映画鑑賞(ファンタジー系)でアマプラにドはまりしてます!

シェアする
記事カテゴリ
最新記事
2024.04.17

Power Apps・Power Automateの勉強方法(1)

2024.04.03

非エンジニア【(元)自治体職員】が半年間ローコード開発をしてみたら

2024.03.27

Power Automateのベストプラクティス・アンチパターン(5)【Apply to each×コンカレンシー×変数の設定はNG】

2024.03.20

Power Apps×Teams:Teams上からPower Appsを実行する方法

2024.03.13

Power Apps:SharePointリストと連携したカレンダーアプリを自作してみよう

モデル駆動型アプリPower AppsPower PlatformSharePointExcelPower AutomateC#attributevalidationローコードAngularAccessInfoPathMatTableAngular Materialデータ構造SortByColumns関数TypeScriptHTMLEF CoreマイグレーションFramework CoreAttribute directivesO/Rマッパーazure sql databaseCase式HTTP RequestCSSxUnit.Net Core 3.1VSCode.Net Core Test ExplorerDataverse for Teamsitem関数Google MapsMarker ClustererRANK()関数Dynamics 365 SalesMicrosoft TranslatorマーカークラスタリングライブラリtailwindcssマルチテナントドロップダウンメニューBreakpointObserverメディアクエリスマホPCレスポンシブ入門初心者中級者キャンバスアプリDatePickerDropdownviewビューアクセス制限承認リマインドSetForAllUpdateContextロードマップ技術It情報技術メッセージIDメールfirst()関数nest入れ子動的リストcollectionコレクション複数の添付ファイル承認フローformエクスポートインポートカスタマイズcomponentダイアログコンポーネントdialogTips新機能変数検索Microsoft 365グループセキュリティグループ送信元メールの送信差出人インスタントクラウドフロー自動化したクラウドフロー委任VBAエラーエクセルerror復元restorePower BI個人列ユーザー列SharePoint Onlineリスト非表示アプリ[市民開発者構築自動化したクラウド フローフローの種類インスタント クラウド フロースケジュール済みクラウド フローレスポンシブ レイアウトresponsive layoutデータ行の制限引き継ぎ退職所有者を変更異動LoopMicrosoftdesignJSONデザインtemplateテンプレート運用選択肢列参照列ChatGPTOpenAIオープンAIチャットGPTgalleryギャラリースクロールコンテナショートカットキーshortcut keyconcat関数文字制限フロー実行開発環境環境本番環境ライセンス環境構築手順pipelineCI/CDパイプラインDevOpsMicrosoft 365簡易在庫管理時間外通知ファイルフィルター クエリドキュメント ライブラリfilter querysortソートmultiple item複数項目シェアポイント便利機能カレンダーCalendarTeamsローコード開発非エンジニア体験談勉強内製化
PageTop
ページトップに戻る