MasaFramework的MinimalAPI設計
- 2022 年 9 月 23 日
- 筆記
- .NET, MASA Framework
在以前的MVC引用程式中,控制器負責接收輸入資訊、執行、編排操作並返迴響應,它是一個功能齊全的框架,它提供了過濾器、內置了模型綁定與驗證,並提供了很多可擴展的管道,但它偏重,不像其它語言是通過更加簡潔的方式來開啟Web之旅的,因此在.Net6.0官方引入了MinimalAPIs,即最小API,與MVC相比,它足夠的簡潔,適合小型服務來使用,下面就讓我們看看如何使用MinimalAPI來開發一個web應用程式
入門
下面我們來看一下官方提供的MinimalAPI
是如何使用的
- 前提條件:安裝.NET 6.0
- 新建ASP.NET Core 空項目
Assignment.MinimalApiDemo
dotnet new web -o Assignment.MinimalApiDemo
cd Assignment.MinimalApiDemo
- 增加一個
Get
請求,修改Program
app.MapGet("/test", () => "Test Success!");
根據需求,自行增加Get
(MapGet)、Post
(MapPost)、Put
(MapPut)、Delete
(MapDelete)方法即可,完整程式碼如下:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/test", () => "Test Success!");
Masa版MinimalAPI
隨著我們的服務變得越來越多,這些服務全部被堆積在Program
中,這樣豈不是變成流水賬式的程式碼?那怎麼做才能使得我們的程式碼更加美觀呢?
下面我們就來看一下Masa提供的MinimalAPIs
是如何來使用的
- 選中項目
Assignment.MinimalApiDemo
,並安裝Masa.Contrib.Service.MinimalAPIs
dotnet add package Masa.Contrib.Service.MinimalAPIs --version 0.6.0-preview.13
- 註冊Masa版的
MinimalAPI
,修改Program
var app = builder.AddServices();
- 新增加一個用戶的服務,新增
UserService
類
public class UserService : ServiceBase
{
public IResult Add(RegisterUserRequest request)
{
//模擬添加用戶
return Results.Ok();
}
}
到這裡已經結束了,可能會有小夥伴十分的疑惑,Masa提供的方案讓我有點摸不著頭腦,但項目運行後就會發現在Swagger上多了一個服務
細心的小夥伴發現了,這個服務好像是我們新增的添加用戶服務
,但鏈接地址為什麼是api/v1/Users
🤔🤔
進階
通過快速入門我們了解到如何使用MinimalAPI
,但我們也清楚流水賬式編程的危害,我們不希望讓項目中充斥著流水賬式的程式碼,我們希望它是整潔的,並且是有跡可循的,這時候Masa提供的MinimalAPI方案進入了我們的視野,它上手難度極低,對我們來說它是很棒的,但如果我們不清楚它是如何設計的話,我們敢放心大膽的使用它嗎?雖然它有些枯燥,但我們必須要掌握它是如何設計的,它都支援了什麼樣的功能
約定
當服務未禁用自動映射路由時,框架會自動掃描繼承ServiceBase
的非抽象子類並註冊到服務集合中(IServiceCollection),並為滿足以下要求的方法自動註冊路由
- 當前類的方法的訪問級別為
public
(不包含父類方法) - 方法上未增加特性
IgnoreRouteAttribute
路由規則
路由規則優先順序:
自定義路由 > 約定生成路由
- 如何自定義路由?
通過RoutePattern
特性我們可以為方法自定義路由
[RoutePattern("user/add")]
public IResult Add([FromBody]RegisterUserRequest request)
{
//模擬添加用戶
return Results.Ok();
}
- 約定的生成路由規則為:
Pattern(路由) = BaseUri + RouteMethodName
BaseUri
: 根地址,默認: null- 當
BaseUri
為空
或者null
時,則BaseUri = Prefix/Version/ServiceName
- 當
RouteMethodName
: 除非自定義RouteMethodName
,否則RouteMethodName = GetMethodName(方法名)
GetMethodName:
- TrimStart:
Get/Post/Create/Put/Update/Delete/Remove
等 - TrimEnd:
Async
PS:/api/v1/User/Add
,將會變成/api/v1/User
當方法的參數存在id並且id支援從Route中獲取時,將會變成/api/v1/User/{id}
,如果id為可空或者存在默認值時,將會變成/api/v1/User/{id?}
配置
配置分為全局配置、局部配置(僅在當前服務生效),其中優先順序為:局部配置 > 全局配置,默認局部配置的參數為null,我們約定局部參數未配置時,以全局配置為準
全局配置
- DisableAutoMapRoute: 是否禁用自動映射路由,如果為true (禁用),則框架不會自動映射路由,默認:false
- Prefix: 前綴,默認: api
- Version: 版本,默認: v1
- AutoAppendId: 是否追加Id,默認: true
- PluralizeServiceName: 服務名稱是否啟用複數,默認: true
- GetPrefixes: 用於識別當前方法類型為
Get
請求,默認:new List<string> { "Get", "Select" }
- PostPrefixes: 用於識別當前方法類型為
Post
請求,默認:new List<string> { "Post", "Add", "Upsert", "Create" }
- PutPrefixes: 用於識別當前方法類型為
Put
請求,默認:new List<string> { "Put", "Update", "Modify" }
- DeletePrefixes: 用於識別當前方法類型為
Delete
請求,默認:new List<string> { "Delete", "Remove" }
- DisableTrimMethodPrefix: 禁用移除方法前綴(
Get/Post/Create/Put/Update/Delete/Remove
等), 默認: false - MapHttpMethodsForUnmatched: 匹配請求方式失敗使用,默認: 支援Post、Get、Delete、Put
- Assemblies: 用於掃描服務所在的程式集,默認:
AppDomain.CurrentDomain.GetAssemblies()
- RouteHandlerBuilder: 基於
RouteHandlerBuilder
的委託,可用於許可權認證、Cors等
局部配置
- BaseUri: 根地址,默認: null
- ServiceName: 自定義服務名,默認: null
- RouteHandlerBuilder:基於
RouteHandlerBuilder
的委託,可用於許可權認證、Cors等 - RouteOptions: 局部路由配置
- DisableAutoMapRoute
- Prefix
- Version
- AutoAppendId
- PluralizeServiceName
- GetPrefixes
- PostPrefixes
- PutPrefixes
- DeletePrefixes
- DisableTrimMethodPrefix
- MapHttpMethodsForUnmatched
其中ServiceName為null時,
ServiceName = 類名.TrimEnd("Service")
//不區分大小寫
特性
RoutePattern
用於自定義路由,支援參數
- Pattern: 自定義路由或自定義方法名
- 當StartWithBaseUri:true,Pattern為自定義方法名
- 當StartWithBaseUri:false,Pattern為自定義路由
- StartWithBaseUri: 是否基於BaseUri進行追加,默認: false
- HttpMethod:請求類型,默認: null(根據方法名前綴自動識別),如果希望指定請求類型而非自動識別,則可手動指定:
Get
、Post
、Put
、Delete
IgnoreRoute
用於忽略方法自動映射,例如;存在某個方法已經手動指定映射路由,不希望框架重複進行映射可使用IgnoreRoute
, 例如:
public class User2Service : ServiceBase
{
public User2Service()
{
App.Map("/api/v2/user/add", Add);
}
[IgnoreRoute]
public void Add([FromBody] RegisterUserRequest request, IData data)
{
data.Add(request.Name, request.Age);
}
}
場景
通過上面的學習我們已經了解到了Masa
提供了哪些配置,那下面就讓我們實戰來演練一下,通過模擬不同的場景使用不同的配置,以確保我們正確掌握這些知識
-
A: 我不是一個新手,從0.6.0版本以前的版本就開始使用
Masa
提供的MinimalAPI
了,對新版的MinimalAPI
很喜歡,但我暫時不希望更改手動註冊的方式,我希望升級之後不會對我現有的項目造成影響,我不希望將升級導致原來的服務無法訪問Q: 您希望繼續使用最新版的
MinimalAPI
,但不希望對原來的項目造成影響,在當前服務中,希望能一如既往的使用手動註冊,而不是自動註冊,那你可以配置全局禁用自動註冊,例如:var app = builder.AddServices(options => { options.DisableAutoMapRoute = true; });
當然如果您希望在某個特定的服務中開啟自動映射,則可以在服務中配置:
public class UserService: ServiceBase { public UserService() { RouteOptions.DisableAutoMapRoute = false; } public void Add([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
-
A: 我是一個新手,我覺得我的項目不需要使用前綴以及版本,我希望自動映射的路由可以幫助我刪掉它們
Q: 你需要的是全局配置,通過全局配置禁用前綴以及版本即可,例如:
var app = builder.AddServices(options => { options.Prefix = string.Empty; options.Version = string.Empty; });
-
A: 我是一個新手,雖然我很想嚴格遵守Resetful標準來寫服務,但遺憾的是我無法掌控全局,總是有人不按照標準對方法進行命名,我希望可以人為控制特定的方法的路由
Q: 目前有兩種方法可供選擇,它們分別是:
第一種:自定義路由並忽略自動映射
public class UserService : ServiceBase { public UserService() { App.Map("/user/add", Add); } [IgnoreRoute] public void Add([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
第二種: 完整自定義路由:
public class UserService : ServiceBase { [RoutePattern("/api/v2/user/add")] public void CreateUser([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
第三種: 僅修改請求方式
public class UserService : ServiceBase { [RoutePattern(HttpMethod = "Post")] public void CreateUser([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
如果您希望手動指定方法的請求類型,則可以使用
[RoutePattern("/api/v2/user/add", HttpMethod = "Post")]
-
A: 我希望為項目中所有的介面都必須授權才能訪問,但我不希望在每個方法上增加
Authorize
特性,那樣太噁心了Q: 您的項目是需要為全局服務來設置,則可通過全局配置的
RouteHandlerBuilder
參數來完成,例如:var app = builder.AddServices(options => { options.RouteHandlerBuilder = routeHandlerBuilder => routeHandlerBuilder.RequireAuthorization(); });
如果您希望對某個服務增加特殊的授權策略,則可以:
public class UserService : ServiceBase { public UserService() { RouteHandlerBuilder = routeHandlerBuilder => routeHandlerBuilder.RequireAuthorization("test"); } public void CreateUser([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
但是你必須知道的是,如果在服務內配置了
RouteHandlerBuilder
,那麼全局配置的RouteHandlerBuilder
將對當前服務失效,局部配置存在時,全局配置將不起作用
-
A: 我希望某個服務不需要經過授權即可訪問,那我該怎麼做?
Q: 只需要在方法上加AllowAnonymous
特性即可, 它是MinimalAPI支援的,除了AllowAnonymous
、EnableCors
、Authorize
等都是支援的, 但HttpGet
、HttpPost
、HttpPut
、HttpDelete
特性是不支援的public class UserService : ServiceBase { [AllowAnonymous] public void CreateUser([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); } }
常見問題
-
為何使用DbContext時總是提示
DbContext
已經被釋放?UserService僅在項目啟動時會被初始化一次,之後不再初始化,因此Service的構造函數參數僅支援
Singleton
或Transient
。如果您的服務的生命周期為Scoped
,建議在對應的方法中增加參數,例如:public void Add([FromBody] RegisterUserRequest request, IData data) { data.Add(request.Name, request.Age); }
-
模型校驗不起作用?
目前版本的
MinimalAPI
並不支援模型綁定與驗證,後續版本會增加支援 -
Builder.AddServices()
又為什麼必須要放到最後?我們知道通過
builder.Build()
可以得到WebApplication
,但在.Net6.0中新增加了限制,這個限制就是在Build
後無法再次更新IServiceCollection
,否則會提示Cannot modify ServiceCollection after application is built
-
為什麼
MinimalAPIs
的生命周期是單例?目前
AddServices
方法中做了兩件事,第一件事就是獲取到所有的服務,並註冊到服務集合中,第二件事就是觸發服務並將對應服務的地址以及方法映射到到App
,App.Map
類似App.Use
,也是一個擴展方法,類似MVC的路由,其生命周期是單例,我們僅僅是將繼承ServiceBase
的服務映射到App
中,並沒有魔改MinimalAPI
,因此並不存在性能問題,但同樣其生命周期也無法改變
總結
MinimalAPI
與MVC
我應該如何選擇?
小型服務使用MinimalAPI
,因為它是很輕量級的,但如果是大型服務或者功能特別複雜的,還是推薦使用MVC
,MinimalAPI
的上手成本很低,但它不是銀彈,選擇適合自己的才是最好的
MinimalAPI還有一些特殊的地方,例如Get
請求無法使用類對象來接收參數,如果希望使用類對象來接受,則需要使用自定義綁定,除此之外還有其他不一樣的地方,完整文檔可查看
本章源碼
Assignment14
//github.com/zhenlei520/MasaFramework.Practice
開源地址
MASA.Framework://github.com/masastack/MASA.Framework
MASA.EShop://github.com/masalabs/MASA.EShop
MASA.Blazor://github.com/BlazorComponent/MASA.Blazor
如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯繫我們