Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

ASP.NET Core Minimal APIのバリデーションを試す (.NET 10 Preview 3)

こんにちは、MLBお兄さんこと松村です。
.NET 10 のプレビューリリースが始まり、先日 Preview 3 が公開されました。そのなかで気になったアップデートについて検証してみました。

devblogs.microsoft.com


ASP.NET Core でのモデルバリデーション

ASP.NET Core では DataAnnotations 属性を使用して、モデルバリデーションを行うことが一般的です。
モデルバリデーションとは、HTTPリクエストで送信されたデータに対して、定義された規則を満たしているかをチェックする処理のことです。

規則のチェックとは、例えば以下のような場合です。

  • 必須項目が入力されているか
  • 年齢欄は数字のみ入力されているか
  • メールアドレス欄にはメールアドレス形式の値が指定されているか
  • 有効期間欄の日付が逆転していないか

このようなチェックは、ASP.NET Core ではこのようなモデルを定義し、フレームワークの機能でバリデーションを構成します。
MVC や Razor Pages などは標準機能としてモデルバリデーションを行うことができます。

using System.ComponentModel.DataAnnotations;

namespace WebApplication1.Models;

public class User : IValidatableObject
{
    [Key]
    public int Id { get; set; }

    [Display(Name = "名前")]
    [Required(ErrorMessage = "{0}が指定されていません。")]
    public string Name { get; set; }

    [Display(Name = "年齢")]
    [Required(ErrorMessage = "{0}が指定されていません。")]
    [RegularExpression(@"^[0-9]+$", ErrorMessage = "{0}は数字のみ入力してください。")]
    public int Age { get; set; }

    [Display(Name = "メールアドレス")]
    [Required(ErrorMessage = "{0}が指定されていません。")]
    [EmailAddress(ErrorMessage = "{0}の形式が不正です。")]
    public string Email { get; set; }

    [Display(Name = "有効期限(開始)")]
    [Required(ErrorMessage = "{0}が指定されていません。")]
    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
    public DateOnly TermFrom { get; set; }

    [Display(Name = "有効期限(終了)")]
    [Required(ErrorMessage = "{0}が指定されていません。")]
    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
    public DateOnly TermTo { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (TermFrom > TermTo)
        {
            yield return new ValidationResult(
                "日付が逆転しています。",
                new[] { nameof(TermFrom), nameof(TermTo) });
        }
    }
}

learn.microsoft.com

Minimal APIでのモデルバリデーション

.NET 9 まで Minimal API ではモデルバリデーションを利用することができず、独自のバリデーションロジックを実装する必要がありました。
※私は Validator.TryValidateObject メソッドを使っていました。

learn.microsoft.com

しかし先日リリースされた .NET 10 Preview 3 にて、ついにモデルバリデーションがサポートされるようになります。
実装方法はこちらのリリースノートに記載されていますが、いまいち分かりにくかったので解説します。

github.com

.NET 10 Preview 3 のインストール

このコードは .NET 10 Preview 3 を前提としていますので、まずは SDK をインストールしましょう。

winget install --id Microsoft.Dotnet.Sdk.Preview

dotnet.microsoft.com

バリデーション無しの基本構成

まず、検証用に Minimal API アプリケーションと Program.cs を準備します。

dotnet new webapi -f net10.0 -o MyApi
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();
app.UseHttpsRedirection();

app.MapPost("/todoitems", (Todo todo) =>
{
    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.Run();

class Todo
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool IsComplete { get; set; }
}

この構成ではまだバリデーションは行われず、名前 (name) を指定しなくてもリクエストは処理されます。
そのため、次のようなバリデーションの構成を適用します。

Program.cs

バリデーションのサポートを有効にします。 > AddValidation メソッド

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddValidation(); // ここ

プロジェクトファイル

csproj ファイルにコード生成を有効にする設定を入れます。 > <InterceptorsNamespaces>

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
      
    <!-- ここ -->
      <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.3.25172.1" />
  </ItemGroup>

</Project>

モデル

モデルクラスにバリデーションを定義します。
このとき、属性での検証と IValidatableObject インターフェースのどちらも使用することができます。

class Todo : IValidatableObject
{
   public int Id { get; set; }

   [Display(Name = "名前")]
   [Required(ErrorMessage = "{0}が指定されていません。")]
   public string? Name { get; set; }

   public bool IsComplete { get; set; }

   public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
   {
       if (!string.IsNullOrWhiteSpace(Name) && !IsComplete)
       {
           yield return new ValidationResult(
               "名前を指定したToDoは完了である必要があります。",
               new[] { nameof(Name), nameof(IsComplete) });
       }
   }
}

動作確認

モデルの定義に沿ったバリデーションが行われ、レスポンスにエラーメッセージが含まれるようになりました。

curl --location 'http://localhost:5247/todoitems/' `
--header 'Content-Type: application/json' `
--data '{
    "id": 0,
    "name": null,
    "isComplete": true
}'

{
  "title": "One or more validation errors occurred.",
  "errors": {
    "Name": [
      "名前が指定されていません。"
    ]
  }
}
curl --location 'http://localhost:5247/todoitems/' `
--header 'Content-Type: application/json' `
--data '{
    "id": 0,
    "name": "yuta",
    "isComplete": false
}'

{
  "title": "One or more validation errors occurred.",
  "errors": {
    "Name": [
      "名前を指定したToDoは完了である必要があります。"
    ]
  }
}

これで MVC や Razor Pages と同じように、DataAnnotation を使ったモデルバリデーションができるようになりました。
個人的にはこのアップデートだけでも .NET 10 を使う理由になると感じています。