一文で理解する.NET Core Web APIの基礎知識

一文で理解する.NET Core Web APIの基礎知識

本記事では.NET Core 3.1を使用してWeb APIの基礎知識を共有します。他の新しいバージョンもほぼ同じです。

最終更新 2022/05/04 14:43
白云任去留
読了目安 24 分
カテゴリ
ASP.NET Core
タグ
.NET C# ASP.NET Core Web API

本文は.NET Core 3.1 を通じて Web API の基礎知識を共有します。他の新しいバージョンもほぼ同じです。

一、はじめに

近年、フロントエンドとバックエンドの分離、マイクロサービスなどのパターンが台頭する中、.NET Core も盛り上がりを見せています。2016 年の最初のバージョンから 2019 年末の 3.1 LTS バージョン、そしてリリース予定の .NET 5 まで、.NET Core は進化を続け、デプロイや開発ツールもクロスプラットフォーム対応を実現しています。.NET Core には以前から注目していましたが、実際の応用にはあまり関わっていませんでした。少し学習と理解を経て、ここに共有します。本文は主に .NET Core Web API を例に、.NET Core の基本的な応用と注意点について説明します。WebAPI を利用してインターフェースアプリケーションを構築したい開発者にとって、システムの概要と認識を提供し、同時に多くの .NET Core 開発者と交流・議論し、探求・修正を重ね、知識への理解を深め、より多くの人を助けることができれば幸いです。本文は基本的な実際の操作に重点を置いており、一部の概念や基本ステップについては詳述しません。文中に誤りや漏れがあれば、ご指摘いただければ幸いです。

二、Swagger による Web API のデバッグ

開発環境:Visual Studio 2019

フロントエンドとバックエンドがインターフェースドキュメントと実際の不一致や、ドキュメントの保守・更新の手間などに悩まされていた問題を解決するため、swagger が登場しました。インターフェーステストの問題も同時に解決します。前置きはこれくらいにして、適用手順を説明します。

  1. 新しい ASP.NET Core Web API アプリケーションを作成し、バージョンは .ASP.NET Core 3.1 を選択します。
  2. NuGet パッケージ Swashbuckle.AspNetCore をインストールします(現在のサンプルバージョンは 5.5.0)。
  3. Startup クラスの ConfigureServices メソッド内に以下の注入コードを追加します:
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "APIドキュメントの説明",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "テストプロジェクト",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKEライセンス",
            //Url = new Uri("http://t.abc.com/")
        }
    });

});

Startup クラスの Configure メソッドに以下のコードを追加します:

// Swaggerの設定
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    c.RoutePrefix = "api"; // 空に設定すると、アクセスパスはルートドメイン/index.htmlになります。空文字列はルートドメインで直接アクセスすることを意味します。別のパスに変更するには、直接名前を書きます。例:c.RoutePrefix = "swagger"; とすると、アクセスパスは ルートドメイン/swagger/index.html になります。
});

Ctrl+F5 でブラウザを開き、上記の設定に従ってパスを http://localhost:***/api/index.html に変更すると、Swagger ページが表示されます:

しかし、これだけでは関連インターフェースのコメント説明は表示されません。XML ファイルを設定してコードを調整します。新しいコードは太字部分です:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "APIドキュメントの説明",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "テストプロジェクト",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKEライセンス",
            //Url = new Uri("http://t.abc.com/")
        }
    });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

上記のコードはリフレクションを使用して Web API プロジェクトに一致する XML ファイル名を生成し、AppContext.BaseDirectory プロパティは XML ファイルのパスを構築します。OpenApiInfo 内の設定パラメータはドキュメントの説明用であり、ここでは詳しく説明しません。

次に、Web API プロジェクトを右クリックし、プロパティ、ビルドの順に選択し、XML ドキュメントの出力パスを設定し、不要な XML コメント警告(1591 を追加)を解除します:

これにより、トリプルスラッシュ(///)でクラス、メソッド、プロパティなどに関連コードのコメントを追加した後、Swagger ページを更新するとコメント説明が表示されます。

XML ファイルを debug ディレクトリの下に出力したくない場合(例えばプロジェクトルートディレクトリに配置したい場合、ただし絶対パスに変更しない)、関連コードを以下のように調整します。xml ファイル名も任意の名前に変更できます:

var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location); // アプリケーションが存在するディレクトリを取得
var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
c.IncludeXmlComments(xmlPath, true);

同時に、プロジェクトの生成で XML ドキュメントファイルのパスを ..\CoreAPI_Demo\CoreAPI_Demo.xml に調整します。

関連インターフェースの非表示

Swagger に公開したくないインターフェースについては、該当する Controller または Action の先頭に [ApiExplorerSettings(IgnoreApi = true)] を追加します。

システムのデフォルト出力パスの調整

プロジェクト起動後、デフォルトで組み込みの weatherforecast にアクセスします。Swagger ドキュメントを直接表示するように変更する場合は、Properties ディレクトリの launchSettings.json ファイルを編集し、launchUrl の値を api(前述の RoutePrefix の値)に変更します:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:7864",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "CoreApi_Demo": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api",
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

三、設定ファイル

appsettings.json ファイルの読み取りを例にします。もちろん他の名前の .json ファイルを定義して読み取ることもでき、読み取り方法は同じです。このファイルは Web.config ファイルに類似しています。サンプル用に appsettings.json ファイルの内容を次のように定義します:

{
  "ConnString": "Data Source=(local);Initial Catalog=Demo;Persist Security Info=True;User ID=DemoUser;Password=123456;MultipleActiveResultSets=True;",
  "ConnectionStrings": {
    "MySQLConnection": "server=127.0.0.1;database=mydemo;uid=root;pwd=123456;charset=utf8;SslMode=None;"
  },
  "SystemConfig": {
    "UploadFile": "/Files",
    "Domain": "http://localhost:7864"
  },
  "JwtTokenConfig": {
    "Secret": "fcbfc8df1ee52ba127ab",
    "Issuer": "abc.com",
    "Audience": "Brooke.WebApi",
    "AccessExpiration": 30,
    "RefreshExpiration": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
  1. 設定ファイルの基本的な読み取り
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // このメソッドはランタイムによって呼び出されます。このメソッドを使用してサービスをコンテナに追加します。
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        // 読み取り方法1
        var ConnString = Configuration["ConnString"];
        var MySQLConnection = Configuration.GetSection("ConnectionStrings")["MySQLConnection"];
        var UploadPath = Configuration.GetSection("SystemConfig")["UploadPath"];
        var LogDefault = Configuration.GetSection("Logging").GetSection("LogLevel")["Default"];

        // 読み取り方法2
        var ConnString2 = Configuration["ConnString"];
        var MySQLConnection2 = Configuration["ConnectionStrings:MySQLConnection"];
        var UploadPath2 = Configuration["SystemConfig:UploadPath"];
        var LogDefault2 = Configuration["Logging:LogLevel:Default"];
    }
}

以上、2 種類の設定情報の読み取り方法を紹介しました。Controller 内で使用する場合も同様に、注入して次のように呼び出します:

public class ValuesController : ControllerBase
{
    private IConfiguration _configuration;

    public ValuesController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    // GET: api/<ValuesController>
    [HttpGet]
    public IEnumerable<string> Get()
    {
        var ConnString = _configuration["ConnString"];
        var MySQLConnection = _configuration.GetSection("ConnectionStrings")["MySQLConnection"];
        var UploadPath = _configuration.GetSection("SystemConfig")["UploadPath"];
        var LogDefault = _configuration.GetSection("Logging").GetSection("LogLevel")["Default"];
        return new string[] { "value1", "value2" };
    }
}
  1. 設定ファイルをカスタムオブジェクトに読み取る

SystemConfig ノードを例に、クラスを次のように定義します:

public class SystemConfig
{
    public string UploadPath { get; set; }
    public string Domain { get; set; }
}

コードを調整します:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // このメソッドはランタイムによって呼び出されます。このメソッドを使用してサービスをコンテナに追加します。
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.Configure<SystemConfig>(Configuration.GetSection("SystemConfig"));
    }
}

次に、Controller 内で注入して呼び出します:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private SystemConfig _sysConfig;
    public ValuesController(IOptions<SystemConfig> sysConfig)
    {
        _sysConfig = sysConfig.Value;
    }

    [HttpGet]
    public IEnumerable<string> GetSetting()
    {
        var UploadPath = _sysConfig.UploadPath;
        var Domain = _sysConfig.Domain;
        return new string[] { "value1", "value2" };
    }
}
  1. 静的クラスにバインドして読み取る

関連する静的クラスを次のように定義します:

public static class MySettings
{
    public static SystemConfig Setting { get; set; } = new SystemConfig();
}

Startup クラスのコンストラクタを次のように調整します:

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

    Configuration = builder.Build();
    //Configuration = configuration;

    configuration.GetSection("SystemConfig").Bind(MySettings.Setting); // 静的設定クラスにバインド
}

これで、MySettings.Setting.UploadPath のように直接使用できます。

四、ファイルアップロード

インターフェースにはファイルアップロードが欠かせません。.NET Framework の webapi では byte 配列オブジェクトなどを使用して複雑な方法でファイルアップロードを行っていましたが、.NET Core WebApi では大きく変わり、新しい IFormFile オブジェクトがアップロードファイルを受け取るために定義されています。Controller コードを直接示します:

バックエンドコード

[Route("api/[controller]/[action]")]
[ApiController]
public class UploadController : ControllerBase
{
    private readonly IWebHostEnvironment _env;

    public UploadController(IWebHostEnvironment env)
    {
        _env = env;
    }

    public ApiResult UploadFile(List<IFormFile> files)
    {
        ApiResult result = new ApiResult();

        // 注:パラメータfilesオブジェクトは、var files = Request.Form.Files; に置き換えて取得することもできます。

        if (files.Count <= 0)
        {
            result.Message = "アップロードファイルは空にできません";
            return result;
        }

        #region アップロード

        List<string> filenames = new List<string>();

        var webRootPath = _env.WebRootPath;
        var rootFolder = MySettings.Setting.UploadPath;

        var physicalPath = $"{webRootPath}/{rootFolder}/";

        if (!Directory.Exists(physicalPath))
        {
            Directory.CreateDirectory(physicalPath);
        }

        foreach (var file in files)
        {
            var fileExtension = Path.GetExtension(file.FileName); // ファイル形式、拡張子を取得

            var saveName = $"{rootFolder}/{Path.GetRandomFileName()}{fileExtension}";
            filenames.Add(saveName); // 相対パス

            var fileName = webRootPath + saveName;

            using FileStream fs = System.IO.File.Create(fileName);
            file.CopyTo(fs);
            fs.Flush();
        }
        #endregion

        result.IsSuccess = true;
        result.Data["files"] = filenames;

        return result;
    }
}

フロントエンドの呼び出し

次に、フロントエンドから上記のアップロードインターフェースを呼び出します。プロジェクトルートに wwwroot ディレクトリ(.NET Core webapi の組み込みディレクトリ)を作成し、関連する js ファイルパッケージを追加し、新規に index.html ファイルを作成します。内容は次のとおりです:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
    <style type="text/css"></style>
    <script src="res/scripts/jquery-1.10.2.min.js"></script>
    <script src="res/scripts/jquery.form.js"></script>
    <script type="text/javascript">
      // 方法1
      function AjaxUploadfile() {
        var upload = $("#files").get(0);
        var files = upload.files;
        var data = new FormData();
        for (var i = 0; i < files.length; i++) {
          data.append("files", files[i]);
        }

        // ここでのdataの構築は、var data = new FormData(document.getElementById("myform")); に置き換えることもできます。

        $.ajax({
          type: "POST",
          url: "/api/upload/uploadfile",
          contentType: false,
          processData: false,
          data: data,
          success: function (result) {
            alert("success");
            $.each(result.data.files, function (i, filename) {
              $("#filePanel").append("<p>" + filename + "</p>");
            });
          },
          error: function () {
            alert("アップロードファイルエラー");
          },
        });
      }

      // 方法2
      function AjaxUploadfile2() {
        $("#myform").ajaxSubmit({
          success: function (result) {
            if (result.isSuccess) {
              $.each(result.data.files, function (i, filename) {
                $("#filePanel").append("<p>" + filename + "</p>");
              });
            } else {
              alert(result.message);
            }
          },
        });
      }
    </script>
  </head>
  <body>
    <form
      id="myform"
      method="post"
      action="/api/upload/uploadfile"
      enctype="multipart/form-data"
    >
      <input type="file" id="files" name="files" multiple /> <br /><br />
      <input
        type="button"
        value="FormData Upload"
        onclick="AjaxUploadfile();"
      /><br /><br />
      <input
        type="button"
        value="ajaxSubmit Upload"
        onclick="AjaxUploadfile2();"
      /><br /><br />
      <div id="filePanel"></div>
    </form>

    <script type="text/javascript">
      $(function () {});
    </script>
  </body>
</html>

上記では、FormData と ajaxSubmit の 2 つの方法でアップロードを実行しています。注意点として、contentTypeprocessData の 2 つのパラメータの設定が必要です。また、一度に複数ファイルをアップロードできるようにするには、multiple 属性を設定します。

wwwroot 以下の静的ファイルにアクセスする前に、Startup クラスの Configure メソッドで登録する必要があります:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles(); // wwwroot 以下のファイルにアクセスするため
}

プロジェクトを起動し、http://localhost:***/index.html にアクセスしてアップロードテストを行います。正常に完了すると、wwwroot の Files ディレクトリにアップロードされたファイルが表示されます。

五、WebApi データ返却形式の統一

統一返却形式の定義

フロントエンドとバックエンドで事前に合意したデータ形式を使用しやすくするため、通常は統一データ返却を定義します。これには成功/失敗、ステータス、具体的なデータなどが含まれます。説明のために、データ返却クラスを次のように定義します:

public class ApiResult
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }
    public string Code { get; set; }
    public Dictionary<string, object> Data { get; set; } = new Dictionary<string, object>();
}

これにより、各 Action インターフェース操作を ApiResult 形式でカプセル化して返却します。新しい ProductController の例を以下に示します:

[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    [HttpGet]
    public ApiResult Get()
    {
        var result = new ApiResult();

        var rd = new Random();

        result.Data["dataList"] = Enumerable.Range(1, 5).Select(index => new
        {
            Name = $"商品-{index}",
            Price = rd.Next(100, 9999)
        });

        result.IsSuccess = true;
        return result;
    }
}
  • Produces:データ返却方法を定義します。各 Controller に [Produces("application/json")] 属性を付けると、JSON 形式でデータ出力されます。
  • ApiController:各 Controller に ApiController 属性があることを確認します。通常、ControllerBase を継承し [ApiController] 属性を付けた基本クラス BaseController を定義し、新しい Controller はこの基本クラスを継承します。
  • Route:ルートアクセス方法。RESTful 形式が好みでない場合は、[Route("api/[controller]/[action]")] のように Action を追加できます。
  • HTTP リクエスト:前述の Swagger 設定と組み合わせて、各 Action に具体的なリクエストメソッド(HttpGet、HttpPost、HttpPut、HttpDelete のいずれか)が必要です。通常は HttpGet と HttpPost で十分です。

これでデータ返却の統一が完了します。

T 形式の日付時刻の解決

.NET Core Web Api はデフォルトで先頭小文字のキャメルケースでクラス名を返しますが、DateTime 型のデータは T 形式の時刻を返します。T 形式を解決するには、日付時刻変換クラスを次のように定義します:

public class DatetimeJsonConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String)
        {
            if (DateTime.TryParse(reader.GetString(), out DateTime date))
                return date;
        }
        return reader.GetDateTime();
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
    }
}

次に、Startup クラスの ConfigureServices で services.AddControllers のコードを次のように調整します:

services.AddControllers()
    .AddJsonOptions(configure =>
    {
        configure.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
    });

六、モデル検証

モデル検証は ASP.NET MVC にもあり、使用方法はほぼ同じです。インターフェースに送信されたデータのパラメータ検証(必須項目、データ形式、文字列長、範囲など)を行います。通常、POST で送信されたオブジェクトはエンティティクラスとして定義して受け取ります。例えば、登録クラスを次のように定義します:

public class RegisterEntity
{
    /// <summary>
    /// 電話番号
    /// </summary>
    [Display(Name = "電話番号")]
    [Required(ErrorMessage = "{0}は必須です")]
    [StringLength(11, ErrorMessage = "{0}は最大{1}文字です")]
    public string Mobile { get; set; }

    /// <summary>
    /// 確認コード
    /// </summary>
    [Display(Name = "確認コード")]
    [Required(ErrorMessage = "{0}は必須です")]
    [StringLength(6, ErrorMessage = "{0}は最大{1}文字です")]
    public string Code { get; set; }

    /// <summary>
    /// パスワード
    /// </summary>
    [Display(Name = "パスワード")]
    [Required(ErrorMessage = "{0}は必須です")]
    [StringLength(16, ErrorMessage = "{0}は最大{1}文字です")]
    public string Pwd { get; set; }
}

Display 属性はフィールドの表示名を指定し、Required は必須項目、StringLength はフィールドの長さを制限します。他にも組み込みの検証属性があります。詳細は公式ドキュメントを参照してください。よく使われる検証属性をいくつか挙げます:

  • [CreditCard]:プロパティがクレジットカード形式であることを検証します。追加の JQuery 検証方法が必要です。
  • [Compare]:モデル内の 2 つのプロパティが一致するかを検証します。
  • [EmailAddress]:プロパティがメール形式であることを検証します。
  • [Phone]:プロパティが電話番号形式であることを検証します。
  • [Range]:プロパティ値が指定した範囲内にあることを検証します。
  • [RegularExpression]:プロパティ値が指定した正規表現と一致するかを検証します。
  • [Required]:フィールドが null でないことを検証します。この属性の動作の詳細については、[Required] 属性を参照してください。
  • [StringLength]:文字列プロパティ値が指定した長さ制限を超えないことを検証します。
  • [Url]:プロパティが URL 形式であることを検証します。
  • [Remote]:サーバー上のアクションメソッドを呼び出して、クライアント上の入力を検証します。

以上が基本的なモデル検証の使用方法です。この方法と T4 テンプレートを組み合わせて、テーブルオブジェクトからモデル検証エンティティを生成することで、アクション内に大量の検証コードを書く手間を省けます。もちろん、より複雑な検証やデータベース操作を伴う検証は、個別にアクションや他のアプリケーションモジュールに記述します。

では、上記のモデル検証は Web API でどのように機能するのでしょうか?Startup クラスの ConfigureServices に以下のコードを追加します:

// モデルパラメータ検証
services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = (context) =>
    {
        var error = context.ModelState.FirstOrDefault().Value;
        var message = error.Errors.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p.ErrorMessage))?.ErrorMessage;

        return new JsonResult(new ApiResult { Message = message });
    };
});

登録サンプルの Action コードを追加します:

/// <summary>
/// 登録
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
public async Task<ApiResult> Register(RegisterEntity model)
{
    ApiResult result = new ApiResult();

    var _code = CacheHelper.GetCache(model.Mobile);
    if (_code == null)
    {
        result.Message = "確認コードの有効期限が切れているか、存在しません";
        return result;
    }
    if (!model.Code.Equals(_code.ToString()))
    {
        result.Message = "確認コードが間違っています";
        return result;
    }

    /**
    関連ロジックコード
    **/
    return result;
}

このように、ApiBehaviorOptions を設定し、検証エラーメッセージの最初の情報を読み取って返却することで、Web API の Action でのリクエストパラメータ検証が完了します。エラーメッセージ Message の返却については、少しカプセル化することもできますが、ここでは省略します。

七、ログの使用

.NET Core WebApi には組み込みのログ管理機能がありますが、必ずしも要件を簡単に満たせるとは限りません。通常はサードパーティのログフレームワーク(代表的なものとして NLog、Log4Net)を使用します。ここでは NLog ログコンポーネントの使用法を簡単に紹介します。

NLog の使用

① NuGet パッケージ NLog.Web.AspNetCore をインストールします(現在のプロジェクトバージョンは 4.9.2)。

② プロジェクトルートに NLog.config ファイルを作成します。NLog.config の詳細な設定については公式ドキュメントを参照してください。ここでは簡易設定を示します:

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Off"
      internalLogFile="NlogRecords.log">
  <!--Nlog内部ログ記録はOffで無効-->
  <extensions>
    <add assembly="NLog.Web.AspNetCore" />
  </extensions>
  <targets>
    <target name="log_file" xsi:type="File" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} | ${level:uppercase=false} | ${message} ${onexception:${exception:format=tostring} ${newline} ${stacktrace} ${newline}" />
  </targets>

  <rules>
    <!--Microsoft コンポーネントのすべてのレベルのログ記録をスキップ-->
    <logger name="Microsoft.*" final="true" />
    <!--<logger name="logdb" writeTo="log_database" />-->
    <logger name="*" minlevel="Trace" writeTo="log_file" />
  </rules>
</nlog>

<!--https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3-->

③ Program.cs ファイルを次のように調整します:

public class Program
{
    public static void Main(string[] args)
    {
        //CreateHostBuilder(args).Build().Run();
        
        var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
        try
        {
            logger.Debug("init main");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception exception)
        {
            //NLog: セットアップエラーをキャッチ
            logger.Error(exception, "例外のためプログラムを停止しました");
            throw;
        }
        finally
        {
            // アプリケーション終了前に内部タイマー/スレッドをフラッシュして停止(Linux でのセグメンテーションフォールトを回避)
            NLog.LogManager.Shutdown();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            }).ConfigureLogging(logging => {
                logging.ClearProviders();
                logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
            }).UseNLog(); // 依存性注入 NLog
}

Main 関数内の例外キャッチコード設定は省略しても構いませんが、CreateHostBuilder の下の UseNLog は必須設定です。

Controller では、注入して次のように呼び出します:

public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        _logger.LogInformation("テストログ");

        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }

ローカルテスト後、debug ディレクトリ下の logs フォルダに生成されたログファイルが確認できます。

八、依存性注入

.NET Core を使用する上で依存性注入は避けて通れません。これは .NET Core の設計思想の一つです。依存性注入(DI)とは何か、なぜ使うのかについての説明はここでは省略し、簡単な例を見てみましょう。

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
}

public class ProductRepository : IProductRepository
{
    public IEnumerable<Product> GetAll()
    {
        // 実装
    }
}

Startup クラスで登録します:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IProductRepository, ProductRepository>();
}

IProductRepository サービスを要求し、GetAll メソッドを呼び出します:

public class ProductController : ControllerBase
{
    private readonly IProductRepository _productRepository;
    public ProductController(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public IEnumerable<Product> Get()
    {
        return _productRepository.GetAll();
    }
}

DI パターンを使用して IProductRepository インターフェースを実装しています。前述の例でもコンストラクタ注入による呼び出しが何度か登場しています。

ライフサイクル

services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, MyDependency>();
  • Transient:リクエストごとに新しいインスタンスを作成します。
  • Scoped:スコープのライフサイクルごとに 1 つのインスタンスを作成します。
  • Singleton:シングルトンパターンで、アプリケーション全体のライフサイクルで 1 つのインスタンスのみ作成します。

実際の業務ロジックのシナリオに応じて適切なライフサイクルサービスを選択する必要があります。

実際のアプリケーションでは、多くのサービスを ConfigureServices に登録する必要があります。1 つずつ記述するのは煩雑で、漏れが発生しやすくなります。一般的には、リフレクションを使用して一括注入し、拡張メソッドを使用して注入する方法が考えられます。例:

public static class AppServiceExtensions
{
    /// <summary>
    /// アプリケーションドメイン内のサービスを登録します
    /// </summary>
    /// <param name="services"></param>
    public static void AddAppServices(this IServiceCollection services)
    {
        var ts = System.Reflection.Assembly.Load("CoreAPI.Data").GetTypes().Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")).ToArray();
        foreach (var item in ts.Where(s => !s.IsInterface))
        {
            var interfaceType = item.GetInterfaces();
            foreach (var typeArray in interfaceType)
            {
                services.AddTransient(typeArray, item);
            }
        }
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddAppServices(); // サービスの一括登録
}

もちろん、この方法でシステム組み込みの DI 注入と組み合わせることで、一括注入の要件を満たせます。しかし、他にも DI 登録を簡略化する方法があります。例えば、Scrutor や Autofac などのサードパーティコンポーネントを使用することもできます。

  1. Scrutor の使用

Scrutor は Microsoft の注入コンポーネントを拡張するライブラリです。簡単な例:

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(s => s.Name.EndsWith("Repository") || s.Name.EndsWith("Service")))
        .AsImplementedInterfaces()
        .WithTransientLifetime()
    );

上記のコードは、Scan メソッドを使用して Repository、Service で終わるインターフェースサービスを一括登録し、ライフサイクルは Transient です。これは前述のリフレクションによる一括登録と同じ効果です。

Scrutor の他の使い方については公式ドキュメントを参照してください。ここでは導入のみ行います。

  1. Autofac

通常、MS 組み込みの DI または Scrutor で十分な場合が多いです。さらに高度な要件(プロパティ注入や MS DI の置き換えなど)がある場合は、Autofac を選択できます。Autofac の具体的な使用法については詳しく説明しません。

九、キャッシュ

MemoryCache の使用

公式の説明によると、開発者はキャッシュを適切に使用し、キャッシュサイズを制限する必要があります。Core ランタイムはメモリ圧力に応じてキャッシュサイズを制限しません。使用方法は、まず登録し、コントローラで呼び出します:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache(); // キャッシュミドルウェア
}
public class ProductController : ControllerBase
{
    private IMemoryCache _cache;

    public ProductController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

    [HttpGet]
    public DateTime GetTime()
    {
        string key = "_timeKey";

        // キャッシュキーを確認
        if (!_cache.TryGetValue(key, out DateTime cacheEntry))
        {
            // キーがキャッシュにないので、データを取得
            cacheEntry = DateTime.Now;

            // キャッシュオプションを設定
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // この時間だけキャッシュに保持し、アクセスがあればリセット
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // データをキャッシュに保存
            _cache.Set(key, cacheEntry, cacheEntryOptions);
        }

        return cacheEntry;
    }
}

上記のコードは時間をキャッシュし、スライディング有効期限(最後のアクセスからの有効期限)を 3 秒に設定しています。絶対有効期限を設定する場合は、SetSlidingExpirationSetAbsoluteExpiration に変更します。ブラウザでリフレッシュすると、3 秒ごとに時間が更新されます。

カプセル化された Cache クラスを添付します:

public class CacheHelper
{
    public static IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());

    /// <summary>
    /// キャッシュ絶対有効期限
    /// </summary>
    ///<param name="key">キャッシュキー</param>
    ///<param name="value">キャッシュする値</param>
    ///<param name="minute">minute 分後に絶対有効期限切れ</param>
    public static void SetChache(string key, object value, int minute)
    {
        if (value == null) return;
        _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(minute)));
    }

    /// <summary>
    /// キャッシュ相対有効期限(最後のアクセスから minute 分後)
    /// </summary>
    ///<param name="key">キャッシュキー</param>
    ///<param name="value">キャッシュする値</param>
    ///<param name="minute">スライディング有効期限(分)</param>
    public static void SetChacheSliding(string key, object value, int minute)
    {
        if (value == null) return;
        _memoryCache.Set(key, value, new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(minute)));
    }

    /// <summary>
    /// キャッシュを設定します。明示的にクリアしない限り、メモリに残り続けます。
    /// </summary>
    ///<param name="key">キャッシュキー</param>
    ///<param name="value">Cache[key] に代入する値</param>
    public static void SetChache(string key, object value)
    {
        _memoryCache.Set(key, value);
    }

    /// <summary>
    /// キャッシュをクリアします
    /// </summary>
    ///<param name="key">キャッシュキー</param>
    public static void RemoveCache(string key)
    {
        _memoryCache.Remove(key);
    }

    /// <summary>
    /// キーに基づいて Cache[key] の値を返します
    /// </summary>
    ///<param name="key"></param>
    public static object GetCache(string key)
    {
        if (key != null && _memoryCache.TryGetValue(key, out object val))
        {
            return val;
        }
        else
        {
            return default;
        }
    }

    /// <summary>
    /// キーに基づいてジェネリックオブジェクトを返します
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <returns></returns>
    public static T GetCache<T>(string key)
    {
        if (key != null && _memoryCache.TryGetValue<T>(key, out T val))
        {
            return val;
        }
        else
        {
            return default;
        }
    }
}

十、例外処理

例外処理ミドルウェアの定義

ここでは主にグローバル例外をキャッチして処理し、ログを記録し、統一された JSON 形式でインターフェース呼び出し元に返却します。例外処理の前にミドルウェアについて簡単に触れておきます。ミドルウェアとは何かについてはここでは詳しく説明しませんが、基本的な構造は次のとおりです:

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await _next(httpContext);
    }
}

以下、独自のグローバル例外処理ミドルウェアを定義します。コードは次のとおりです:

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustomExceptionMiddleware> _logger;

    public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "未処理の例外...");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext httpContext, Exception ex)
    {
        var result = JsonConvert.SerializeObject(new { isSuccess = false, message = ex.Message });
        httpContext.Response.ContentType = "application/json;charset=utf-8";
        return httpContext.Response.WriteAsync(result);
    }
}

/// <summary>
/// 拡張メソッドでミドルウェアを追加します
/// </summary>
public static class CustomExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomExceptionMiddleware>();
    }
}

次に、Startup クラスの Configure メソッドで上記の拡張ミドルウェアを追加します(太字部分):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // グローバル例外処理
    app.UseCustomExceptionMiddleware();
}

HandleExceptionAsync メソッドでは、開発とテストの便宜のためシステムエラーを返していますが、本番環境では固定のエラーメッセージを返すように統一することもできます。

例外ステータスコードの処理

HTTP ステータスコードについて、通常の 200 の他に、401、403、404、502 などがあります。システムが常に 200 を返すとは限らず、200 以外のステータスコードの場合も WebApi で適切に処理し、インターフェース呼び出し元が正しく受け取れるようにする必要があります。例えば、次の「JWT 認証」で認証トークンの有効期限切れや権限がない場合、システムは 401 や 403 を返しますが、インターフェースは有効な返却を提供しません。そのため、ここではよくある例外ステータスコードを列挙し、200 形式でインターフェース呼び出し元に提供します。Startup クラスの Configure メソッドに以下のコードを追加します:

app.UseStatusCodePages(async context =>
{
    //context.HttpContext.Response.ContentType = "text/plain";
    context.HttpContext.Response.ContentType = "application/json;charset=utf-8";

    int code = context.HttpContext.Response.StatusCode;
    string message =
        code switch
        {
            401 => "未ログイン",
            403 => "アクセス拒否",
            404 => "見つかりません",
            _ => "不明なエラー",
        };

    context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
    await context.HttpContext.Response.WriteAsync(Newtonsoft.Json.JsonConvert.SerializeObject(new
    {
        isSuccess = false,
        code,
        message
    }));
});

コードは非常にシンプルです。組み込みの例外処理ミドルウェア UseStatusCodePages を使用しています。もちろん、カスタムフィルターで例外を処理することもできますが、お勧めしません。シンプルで効率的で直接的な方法が求められます。

.NET Core の例外処理ミドルウェアには他にも UseExceptionHandlerUseStatusCodePagesWithRedirects などがあります。各ミドルウェアには適した環境があり、MVC や他のアプリケーションシナリオに適したものを選べばよいでしょう。

余談:UseStatusCodePages を使った例外ステータスコード処理を、前述のグローバル例外処理ミドルウェアにカプセル化することもできます。

十一、アプリケーションセキュリティと JWT 認証

JWT とは何かについてはここでは説明しません。実際のアプリケーションでは、一部のインターフェースのセキュリティを確保するため、例えば認証が必要なインターフェースリソースに対して、Web API では通常トークンベースの認証を行い、サーバー側でキャッシュと組み合わせて実現します。

では、なぜ JWT 認証を選ぶのでしょうか?理由は以下の通りです:サーバー側で保存しない、ステートレス、モバイル向け、分散環境に適している、標準化されているなど。JWT の使用方法は次のとおりです:

NuGet パッケージ Microsoft.AspNetCore.Authentication.JwtBearer をインストールします(現在のサンプルバージョンは 3.1.5)。

ConfigureServices で注入します。デフォルトでは Bearer という名前ですが、他の名前に変更しても構いません。前後で一致させてください。太字部分に注意:

appsettings.json に JWT 設定ノードを追加します(前述の「設定ファイル」参照)。JWT 関連の認証クラスを追加します:

public static class JwtSetting
{
    public static JwtConfig Setting { get; set; } = new JwtConfig();
}

public class JwtConfig
{
    public string Secret { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public int AccessExpiration { get; set; }
    public int RefreshExpiration { get; set; }
}

静的クラスにバインドする方法で JWT 設定を読み取り、注入します:

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    //Configuration = configuration;

    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

    Configuration = builder.Build();

    configuration.GetSection("SystemConfig").Bind(MySettings.Setting); // 静的設定クラスにバインド
    configuration.GetSection("JwtTokenConfig").Bind(JwtSetting.Setting); // 同上
}

public IConfiguration Configuration { get; }

// このメソッドはランタイムによって呼び出されます。このメソッドを使用してサービスをコンテナに追加します。
public void ConfigureServices(IServiceCollection services)
{
    #region JWT認証注入

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    services.AddAuthentication("Bearer")
        .AddJwtBearer("Bearer", options =>
        {
            options.RequireHttpsMetadata = false;

            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = JwtSetting.Setting.Issuer,
                ValidAudience = JwtSetting.Setting.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret))
            };
        });

    #endregion
}

Swagger に JWT 認証サポートを追加します。完了すると、Swagger ページに鍵アイコンが表示され、トークンを取得して Value(Bearer token 形式)に入力して Authorize ログインできるようになります。Swagger の JWT 設定は太字部分:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "APIドキュメントの説明",
        Contact = new OpenApiContact
        {
            Email = "5007032@qq.com",
            Name = "テストプロジェクト",
            //Url = new Uri("http://t.abc.com/")
        },
        License = new OpenApiLicense
        {
            Name = "BROOKEライセンス",
            //Url = new Uri("http://t.abc.com/")
        }
    });

    // Swagger JSON および UI の XML ドキュメントコメントパスを設定
    //var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location); // アプリケーションが存在するディレクトリを取得(作業ディレクトリの影響を受けない)
    //var xmlPath = Path.Combine(basePath, "CoreAPI_Demo.xml");
    //c.IncludeXmlComments(xmlPath, true);

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);

    #region JWT認証Swagger承認

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT認証(データはリクエストヘッダーで送信されます) 直接下のボックスに Bearer {token}(スペースあり)を入力してください",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] { }
        }
    });

    #endregion
});

Starup クラスに Configure の登録を追加します。app.UseAuthorization(); の前に配置する必要があります:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseAuthentication(); // jwt認証

    app.UseAuthorization();
}

これで JWT の基本設定は完了です。次に認証ログインと認可を実装します。シミュレーション操作は次のとおりです:

[HttpPost]
public async Task<ApiResult> Login(LoginEntity model)
{
    ApiResult result = new ApiResult();

    // ユーザー名とパスワードの検証
    var userInfo = await _memberService.CheckUserAndPwd(model.User, model.Pwd);
    if (userInfo == null)
    {
        result.Message = "ユーザー名またはパスワードが正しくありません";
        return result;
    }
    var claims = new Claim[]
    {
        new Claim(ClaimTypes.Name, model.User),
        new Claim(ClaimTypes.Role, "User"),
        new Claim(JwtRegisteredClaimNames.Sub, userInfo.MemberID.ToString()),
    };

    var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(JwtSetting.Setting.Secret));
    var expires = DateTime.Now.AddDays(1);
    var token = new JwtSecurityToken(
                issuer: JwtSetting.Setting.Issuer,
                audience: JwtSetting.Setting.Audience,
                claims: claims,
                notBefore: DateTime.Now,
                expires: expires,
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

    // Token 生成
    string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

    // 最終ログイン時間を更新
    await _memberService.UpdateLastLoginTime(userInfo.MemberID);

    result.IsSuccess = 1;
    result.ResultData["token"] = jwtToken;
    result.Message = "認証成功!";
    return result;
}

上記のコードはログイン操作をシミュレートしています(アカウント・パスワードでログインし、成功後 1 日の有効期限を設定)。トークンを生成して返却し、フロントエンド呼び出し元はトークンを localstorage などに保存し、認証が必要なインターフェースを呼び出す際に、そのトークンをヘッダー(Bearer token)に追加してリクエストします。次に、認証が必要な Controller または Action に Authorize 属性を付けます:

[Authorize]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

ロールベースの認可を追加する場合は、次のように制限できます:

[Authorize(Roles = "user")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

// 複数ロールはカンマ区切り
[Authorize(Roles = "Administrator,Finance")]
[Route("api/[controller]/[action]")]
public class UserController : ControllerBase
{
}

異なるロール情報は、ログイン時に ClaimTypes.Role を設定して構成します。これは単純なロールサービスの例ですが、複雑なものはポリシーサービスを登録し、データベースと組み合わせて動的に構成することもできます。

これで、簡単な JWT ベースの認証認可の仕組みが完成しました。

十二、クロスオリジン

フロントエンドとバックエンドの分離では、クロスオリジンの問題が発生します。簡単なクロスオリジン対応は次のとおりです:

拡張サポートを追加

public static class CrosExtensions
{
    public static void ConfigureCors(this IServiceCollection services)
    {
        services.AddCors(options => options.AddPolicy("CorsPolicy",
            builder =>
            {
                builder.AllowAnyMethod()
                    .SetIsOriginAllowed(_ => true)
                    .AllowAnyHeader()
                    .AllowCredentials();
            }));

        //services.AddCors(options => options.AddPolicy("CorsPolicy",
        //builder =>
        //{
        //    builder.WithOrigins(new string[] { "http://localhost:13210" })
        //        .AllowAnyMethod()
        //        .AllowAnyHeader()
        //        .AllowCredentials();
        //}));
    }
}

Startup クラスに関連登録を追加します:

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureCors();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCors("CorsPolicy"); // クロスオリジン
}

これで簡単なクロスオリジン対応は完了です。WithOriginsWithMethods などを設定して、リクエスト元やリクエスト方法を制限することもできます。

以上で全編終了です。この記事に関連するソースコード:https://github.com/Brooke181/CoreAPI_Demo

次回は Dapper を .NET Core で使用する方法を紹介します。ご支援ありがとうございます。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2022/06/22

ASP.NET Core WebAPI でローカリゼーションを実装する(単一リソースファイル)

Microsoft のデフォルトは、1 つのクラスに複数のリソースファイルを対応させる方法であり、使用がやや面倒です。本記事では、単一リソースファイルの使用方法を紹介します。つまり、プロジェクト全体のすべてのクラスが 1 セットの多言語リソースファイルに対応します。

続きを読む
同じカテゴリ / 同じタグ 2022/04/13

ASP.NET Core WebApiの戻り結果統一包装の実践

WebApiの結果を統一して返す際に、より良い制限方法や、よりシンプルで強力な結果のラッピングについて考えるようになりました。絶えず考えを巡らせ改善する中で、ようやく初歩的な成果を得たので共有します。学びに終わりはなく、考えにも終わりはありません。皆様と共に励みたいと思います。

続きを読む