書接上回,上一章介紹了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