zukucode
主にWEB関連の情報を技術メモとして発信しています。

C# 文字数制限を共有して追加・編集の条件がずれるのを防ぐ

商品名を40文字まで入力できるようにしたのに、追加画面では40文字、編集APIでは30文字までになってしまうことがあります。

同じ項目の制限を別々に書いていると、仕様変更時に一部だけ修正し忘れるためです。

今回は、文字数制限の置き場所を決めて、複数の入口から同じ値を参照する方法を紹介します。C#のデータ定義と検証部分に絞った例です。

数字ではなく業務上の意味でまとめる

次の2つは、どちらも40文字までだったとしても、別のルールかもしれません。

  • 商品名は40文字まで。
  • 会員の表示名は40文字まで。

商品名だけを60文字にしたくなったとき、同じ定数を使っていると会員の表示名まで変わります。

共通化するのは、同じ数字ではなく「商品名の上限」という意味です。追加と編集で同じ商品名を扱うなら、その上限を共有します。

値を作る側に上限を置く

ProductName.cs
using System;

public sealed class ProductName
{
    public const int MaxLength = 40;
    public string Value { get; }

    private ProductName(string value)
    {
        Value = value;
    }

    public static ProductName From(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("商品名は必須です。", nameof(value));

        if (value.Length > MaxLength)
            throw new ArgumentException("商品名が長すぎます。", nameof(value));

        return new ProductName(value);
    }
}

MaxLengthを公開し、外から参照できる定数にします。値の生成もFromへ集め、上限を超えた商品名を通常の呼び出し方で作れないようにしています。

この例では前後の空白を除去せず、空白も長さに含めます。正規化まで同時に扱うと、検証前と検証後で長さが変わるため、まずは数え方をそろえた例にしています。

入力用の型も同じ上限を参照する

ProductInputs.cs
using System.ComponentModel.DataAnnotations;

public sealed class CreateProductInput
{
    [Required]
    [StringLength(ProductName.MaxLength)]
    public string Name { get; set; } = "";
}

public sealed class UpdateProductInput
{
    [Required]
    [StringLength(ProductName.MaxLength)]
    public string Name { get; set; } = "";
}

数値の40を繰り返さず、ProductName.MaxLengthを参照します。入力用の型は、APIなどで受け取るデータを表すものです。

属性による入力チェックは、対応する検証処理から呼ばれて初めて働きます。通常のC#コードでnew CreateProductInput()を呼んだだけでは検証されません。

ASP.NET Coreでの入力チェックの基本は、以下の記事を参照してください。

APIの入力値を検証する方法

2か所で確認する理由を分ける

入力用の検証とProductName.Fromの両方で長さを確認すると、重複に見えるかもしれません。

検証する場所役割
APIの入力用の型利用者へ入力エラーを返す
ProductName.FromAPI以外から呼ばれても、不正な値を作らせない

管理ツールやバッチから商品名を作る場合、APIの入力チェックを通るとは限りません。ルールの適用場所は複数あっても、上限の定義は同じものを使えます。

ただし、Fromの例外をそのままAPIへ投げれば適切な入力エラーになる、という意味ではありません。APIでは入力検証を行い、想定したエラー形式へ変換する必要があります。

空白を削除するなら検証の順番もそろえる

40文字の商品名の前に空白が1つある場合、空白を含めると41文字、Trimした後なら40文字です。

API側は加工前の長さ、値を作る側は加工後の長さを見ていると、同じ上限でも結果がずれます。

空白を除去する仕様なら、「正規化してから検証する」という順序を各入口でそろえます。定数を共有するだけで、入力の扱い全体が統一されるわけではありません。

何を1文字と数えるかも決める

このコードのstring.LengthはUTF-16のコード単位を数えます。絵文字などは、見た目の1文字が複数として数えられる場合があります。

StringLengthによる文字列の長さ検証もこの数え方に合わせた例です。「見た目で40文字」を要件にする場合は、別の数え方と検証が必要です。

また、UTF-8で保存するときのバイト数制限とも異なります。データベースや連携先の制限がどの単位なのかを確認します。

フロントエンドへは自動で伝わらない

C#の定数を変えても、TypeScriptで手書きした入力欄の上限は変わりません。

API仕様へ制約が出力されるなら、その情報を利用する方法があります。ただし、型の生成だけでフォームの上限まで設定されるとは限りません。使う生成ツールとフォーム実装の対応を確認します。

APIの型と通信コードを生成するときの分担

上限変更時に確認すること

入力・変更確認する内容
40文字追加と編集の両方で受け付ける
41文字両方で拒否する
空白だけ必須エラーになる
APIを通さず値を作る不正な長さを拒否する
上限を変更する入力欄、DB、外部連携の制限も見直す

このサンプルは.NET 8以降で使えます。定数を共有する狙いは、修正すべき数字を減らすことです。項目の意味、加工の順番、文字の数え方まで説明できる形にしておくと、変更時の判断もしやすくなります。


関連記事