.NET9 - Swagger平替Scalar詳解(四)

IT规划师發表於2024-11-26

書接上回,上一章介紹了Swagger代替品Scalar,在使用中遇到不少問題,今天單獨分享一下之前Swagger中常用的功能如何在Scalar中使用。

下面我們將圍繞文件版本說明、介面分類、介面描述、引數描述、列舉型別、檔案上傳、JWT認證等方面詳細講解。

01、版本說明

我們先來看看預設新增後是什麼樣子的。

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllers();
    builder.Services.AddOpenApi();
    var app = builder.Build();
    app.MapScalarApiReference();
    app.MapOpenApi();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
}

效果如下:

我們可以直接修改builder.Services.AddOpenApi()這行程式碼,修改這塊描述,程式碼如下:

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "訂單微服務",
            Version = "v1",
            Description = "訂單相關介面"
        };
        return Task.CompletedTask;
    });
});

我們再來看看效果。

02、介面分類

透過上圖可以看到選單左側排列著所有介面,現在我們可以透過Tags特性對介面進行分類,如下圖我們把增刪改查4個方法分為冪等介面和非冪等介面兩類,如下圖:

然後我們看看效果,如下圖:

03、介面描述

之前使用Swagger我們都是透過生成的註釋XML來生成相關介面描述,現在則是透過編碼的方式設定後設資料來生成相關描述。

可以透過EndpointSummary設定介面摘要,摘要不設定預設為介面url,透過EndpointDescription設定介面描述,程式碼如下:

//獲取
[HttpGet(Name = "")]
[Tags("冪等介面")]
[EndpointDescription("獲取訂單列表")]
public IEnumerable<Order> Get()
{
    return null;
}
//刪除
[HttpDelete(Name = "{id}")]
[Tags("冪等介面")]
[EndpointSummary("刪除訂單")]
[EndpointDescription("根據訂單id,刪除相應訂單")]
public bool Delete(string id)
{
    return true;
}

執行效果如下:

04、引數描述

同樣可以透過Description特性來設定引數的描述,並且此特性可以直接作用於介面中引數之前,同時也支援作用於屬性上,可以看看下面示例程式碼。

public class Order
{
    [property: Description("建立日期")]
    public DateOnly Date { get; set; }
    [property: Required]
    [property: DefaultValue(120)]
    [property: Description("訂單價格")]
    public int Price { get; set; }
    [property: Description("訂單折扣價格")]
    public int PriceF => (int)(Price * 0.5556);
    [property: Description("商品名稱")]
    public string? Name { get; set; }
}
[HttpPut(Name = "{id}")]
[Tags("非冪等介面")]
public bool Put([Description("訂單Id")] string id, Order order)
{
    return true;
}

效果如下圖:

從上圖可以發現除了描述還有預設值、必填項、可空等字樣,這些是透過其他後設資料設定的,對於屬性還有以下後設資料可以進行設定。

05、列舉型別

對於列舉型別,我們正常關注兩個東西,其一為列舉項以int型別展示還是以字串展示,其二為列舉項顯示描述資訊。

關於第一點比較簡單隻要對列舉型別使用JsonStringEnumConverter即可,程式碼如下:

[JsonConverter(typeof(JsonStringEnumConverter<OrderStatus>))]
public enum OrderStatus
{
    [Description("等待處理")]
    Pending = 1,
    [Description("處理中")]
    Processing = 2,
    [Description("已發貨")]
    Shipped = 3,
    [Description("已送達")]
    Delivered = 4,
}

效果如下:

透過上圖也可以引發關於第二點的需求,如何對每個列舉項新增描述資訊。

要達到這個目標需要做兩件事,其一給每個列舉項透過Description新增後設資料定義,其二我們要修改文件的資料結構Schema。

修改builder.Services.AddOpenApi(),透過AddSchemaTransformer方法修改文件資料結構,程式碼如下:

options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
    //找出列舉型別
    if (context.JsonTypeInfo.Type.BaseType == typeof(Enum))
    {
        var list = new List<IOpenApiAny>();
        //獲取列舉項
        foreach (var enumValue in schema.Enum.OfType<OpenApiString>())
        {
            //把列舉項轉為列舉型別
            if (Enum.TryParse(context.JsonTypeInfo.Type, enumValue.Value, out var result))
            {
                //透過列舉擴充套件方法獲取列舉描述
                var description = ((Enum)result).ToDescription();
                //重新組織列舉值展示結構
                list.Add(new OpenApiString($"{enumValue.Value} - {description}"));
            }
            else
            {
                list.Add(enumValue);
            }
        }
        schema.Enum = list;
    }
    return Task.CompletedTask;
});

我們再來看看結果。

但是這也帶來了一個問題,就是引數的預設值也是新增描述的格式,顯然這樣的資料格式作為引數肯定是錯誤的,因此我們需要自己注意,如下圖。目前我也沒有發現更好的方式即可以把每項列舉描述加上,又不影響引數預設值,有解決方案的希望可以不吝賜教。

06、檔案上傳

下面我們來看看檔案上傳怎麼用,直接上程式碼:

[HttpPost("upload/image")]
[EndpointDescription("圖片上傳介面")]
[DisableRequestSizeLimit]
public bool UploadImgageAsync(IFormFile file)
{
    return true;
}

然後我們測試一下效果。

首先我們可以看到請求示例中相關資訊,這個相當於告訴我們後面要怎麼選擇檔案上傳,我們繼續點選Test Request。

首先請求體需要選擇multipart/form-data,上圖請求示例中已經給出提示。

然後設定key為file,上圖請求示例中已經給出提示,然後點選File上傳圖片,最後點選Send即可。

07、JWT認證

最後我們來看看如何使用JWT認證,

首先我們需要注入AddAuthentication及AddJwtBearer,具體程式碼如下:

public class JwtSettingOption
{
    //這個字元數量有要求,不能隨便寫,否則會報錯
    public static string Secret { get; set; } = "123456789qwertyuiopasdfghjklzxcb";
    public static string Issuer { get; set; } = "asdfghjkkl";
    public static string Audience { get; set; } = "zxcvbnm";
    public static int Expires { get; set; } = 120;
    public static string RefreshAudience { get; set; } = "zxcvbnm.2024.refresh";
    public static int RefreshExpires { get; set; } = 10080;
}
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    //取出私鑰
    var secretByte = Encoding.UTF8.GetBytes(JwtSettingOption.Secret);
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        //驗證釋出者
        ValidateIssuer = true,
        ValidIssuer = JwtSettingOption.Issuer,
        //驗證接收者
        ValidateAudience = true,
        ValidAudiences = new List<string> { JwtSettingOption.Audience, JwtSettingOption.Audience },
        //驗證是否過期
        ValidateLifetime = true,
        //驗證私鑰
        IssuerSigningKey = new SymmetricSecurityKey(secretByte),
        ClockSkew = TimeSpan.FromHours(1), //過期時間容錯值,解決伺服器端時間不同步問題(秒)
        RequireExpirationTime = true,
    };
});

然後我們需要繼續修改builder.Services.AddOpenApi()這行程式碼,在裡面加上如下程式碼:

其中BearerSecuritySchemeTransformer實現如下:

public sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == JwtBearerDefaults.AuthenticationScheme))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                [JwtBearerDefaults.AuthenticationScheme] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = JwtBearerDefaults.AuthenticationScheme.ToLower(),
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Id = JwtBearerDefaults.AuthenticationScheme,
                            Type = ReferenceType.SecurityScheme
                        }
                    }] = Array.Empty<string>()
                });
            }
        }
    }
}

下面就可以透過[Authorize]開啟介面認證,並實現一個登入介面獲取token用來測試。

[HttpPost("login")]
[EndpointDescription("登入成功後生成token")]
[AllowAnonymous]
public string  Login()
{
    //登入成功返回一個token
    // 1.定義需要使用到的Claims
    var claims = new[] { new Claim("UserId", "test") };
    // 2.從 appsettings.json 中讀取SecretKey
    var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSettingOption.Secret));
    // 3.選擇加密演算法
    var algorithm = SecurityAlgorithms.HmacSha256;
    // 4.生成Credentials
    var signingCredentials = new SigningCredentials(secretKey, algorithm);
    var now = DateTime.Now;
    var expires = now.AddMinutes(JwtSettingOption.Expires);
    // 5.根據以上,生成token
    var jwtSecurityToken = new JwtSecurityToken(
        JwtSettingOption.Issuer,         //Issuer
        JwtSettingOption.Audience,       //Audience
        claims,                          //Claims,
        now,                             //notBefore
        expires,                         //expires
        signingCredentials               //Credentials
    );
    // 6.將token變為string
    var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
    return token;
}

下面我們先用登入介面獲取一個token。

我們先用token呼叫介面,可以發現返回401。

然後我們把上面獲取的token放進去,請求成功。

在這個過程中有可能會遇到一種情況:Auth Type後面的下拉框不可選,如下圖。

可能因以下原因導致,缺少[builder.Services.AddAuthentication().AddJwtBearer();]或[options.AddDocumentTransformer();]任意一行程式碼。

:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

相關文章