前言

ASP.NET core 的名字還沒記熟它又要改版本名了,不過對未來的發展這或許是好事吧。依據 roadmap 所示未來的版本名稱就會比較清楚一點。而在 .net 發長趨於穩定時開發應用時讓你又愛又恨的文件撰寫,你還在獨立寫一份嗎?程式碼跟文件之間的更新成本就不能在壓低一點嗎?若你還沒聽過 NSwag 的話這是一個你值得做的嘗試,另外不可或缺的就是 log 系統,你還在用 NLog 或是內建的日誌 lib 嗎?不體驗一下 Serilog 就太可惜了,尤其是目前應用多數跑在 container 裡,在系統 micro-service 化的現代裡,將 log 寫在本機裡在 container 數量漸多後管理上是有點麻煩,而 serilog 的外掛可以幫你把 log 直接送去指定的地方。

內文

本文將會基於 Asp.NET Core 3.1 版本來開發,雖然 .NET 5 已經推出但是它並非 LTS 版本所以並非首選。

本文的 source code 在 這裡

第一個步驟就是把必要的 Packages 裝一裝

1
dotnet add .\AspNetCoreSerilog\AspNetCoreSerilog.csproj package Serilog.AspNetCore

appsetting.json 直接將原本的 Logging 區塊取代為以下設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "Serilog": {
    "Using": [ "Serilog.Sinks.Console" ],
    "MinimumLevel": "Information",
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
    "Properties": {
      "Application": "AspNetCoreSerilog"
    }
  }
}

而在 Program.cs 也就是入口程式那邊補上以下程式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 抓取 serilog 設定檔的內容
var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true)
                .Build();
// 讓 serilog 使用設定檔的內容來運作
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(configuration)
    .CreateLogger();

這樣就是一個基本的 serilog 設定完成了,接下來安裝 NSwag 的相關套件

1
dotnet add .\AspNetCoreSerilog\AspNetCoreSerilog.csproj package NSwag.AspNetCore

接下來進行基本的設定就 NSwag 就算成功在系統上 run 起來了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void ConfigureServices(IServiceCollection services)
{
    // 加上以下這一段設定
    services.AddOpenApiDocument(
                document =>
                {
                    document.Title = "AspNetCore NSwag";
                    document.Description = "Demo for NSwag on ASP.NET Core";
                    document.DocumentName = "v1";// 預設值為 v1
                });
}
1
2
3
4
5
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 註冊 OpenApi 文件產生的 middleware
    app.UseOpenApi();
}

在設定並註冊完上述的設定後,可以在 local run 起來以下的位置,就可以看到 NSwag 把你該系統的 API 都抓出來了

curl -X GET "{FQDN}/swagger/v1/swagger.json" -H "accept: application/json"

實際看過這份文件之後應該跟你想像中好文件的差距頗大,它需要更炫的 UI !而這當然也有 package 可以幫我們。很幸運的是這個 package 其實就是整在我們剛剛裝的那個 package 裡,所以設定也只需要多加一行

1
2
3
4
5
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 註冊 Swagger UI
    app.UseSwaggerUi3();
}

curl -X GET "{FQDN}/swagger" -H "accept: application/json"

這樣就可以了,也會是最多人在被推薦這套 package 時最常看到的模樣,不過看起來他只是幫你把你的 API 列出來並且有一個介面可以打,目前看起來還是比較像是本機的高級除錯工具,而跟線上文件比較無關,接下來就是要讓 Swagger 可以吃定義在 Model 身上的描述。

1
2
3
4
5
6
7
8
9
[HttpGet("{days}")]
[OpenApiOperation("This is operation summary","This is operation description")]
[ProducesResponseType(typeof(WeatherForecast), 200)]
public IEnumerable<WeatherForecast> Get(
       [Required][FromRoute][Description("days that will show")]
       int days)
{
    // some logic in controller
}

主要標籤 OpenApiOperation Source Code

我們在 Action 身上補上 OpenApiOperation 標籤,這個標籤所接受的兩個參數就會是 Swagger 介面中我們會看到的 Api 概述與詳述。 而參數的部分可以使用既有的 ComponentModel attribute: DescriptionAttribute 就可以讓 Swagger 吃到,這樣就可以了。

不過很多情況下 API 會要先通過一些授權機制才能正常呼叫,而 Swagger 也需要做點調整才能正常呼叫。以下是目前 Swagger 支援的方式:

  • API key
  • HTTP
  • OAuth 2.0
  • Open ID Connect

這邊就用最常見的 JWT 授權方式來演示一下作法,在原本的 Startup.cs 裡的 void ConfigureServices(IServiceCollection services) 裡的 AddOpenApiDocument 中插入以下片段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var apiScheme = new OpenApiSecurityScheme()
{
    Type = OpenApiSecuritySchemeType.Http,
    Scheme = "Bearer",
    BearerFormat = "JWT",
    Description = "JWT Token Auth"
};

// empty string array 這邊直接指明 new string[]{"Bearer"} 也是可以的
config.AddSecurity("Bearer", Enumerable.Empty<string>(), apiScheme);
config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor());

這樣補上去之後 Swagger 在發送 request 時就會補上 Authoriztion 為 Bearer 的 header。

這邊我們先實做一個自己的 Auth 機制,為求簡單裡面沒有真的檢查任何東西

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;
using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;
// Attribute, 若不是全域註冊的話會需要繼承
public class MyAuth : Attribute, IAuthorizationFilter
{
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var authorization = context.HttpContext.Request.Headers[HeaderNames.Authorization];

            if (AuthenticationHeaderValue.TryParse(authorization, out var authValue))
            {
                if(string.Equals(authValue.Scheme, JwtBearerDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
                {
                    return;
                }
            }

            context.Result = new ForbidResult(JwtBearerDefaults.AuthenticationScheme);
        }
}

然後在 Startup.cs 中將這個 Auth 註冊進去,這邊採用的註冊方式是全域註冊,若要 by controller or action 的話就要繼承 Attribute

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void ConfigureServices(IServiceCollection services)
{
    //...其他的註冊
    // Add this when you use Jwt Bearer in asp.net core
    services.AddAuthentication().AddJwtBearer();
    services.AddAuthorization();
    // Add MyAuth as a global auth filter
    services.AddControllers(option =>
    {
        option.Filters.Add(typeof(MyAuth));
    });
    //...其他的註冊
}

以上大概就是基本的 NSwag 的一些設定,若提供的 API 是內部在呼叫的話,其實這些設定應該就會滿夠用的。

Reference