MasaFramework的MinimalAPI設計

在以前的MVC引用程式中,控制器負責接收輸入資訊、執行、編排操作並返迴響應,它是一個功能齊全的框架,它提供了過濾器、內置了模型綁定與驗證,並提供了很多可擴展的管道,但它偏重,不像其它語言是通過更加簡潔的方式來開啟Web之旅的,因此在.Net6.0官方引入了MinimalAPIs,即最小API,與MVC相比,它足夠的簡潔,適合小型服務來使用,下面就讓我們看看如何使用MinimalAPI來開發一個web應用程式

入門

下面我們來看一下官方提供的MinimalAPI是如何使用的

  1. 新建ASP.NET Core 空項目Assignment.MinimalApiDemo
dotnet new web -o Assignment.MinimalApiDemo
cd Assignment.MinimalApiDemo
  1. 增加一個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是如何來使用的

  1. 選中項目Assignment.MinimalApiDemo,並安裝Masa.Contrib.Service.MinimalAPIs
dotnet add package Masa.Contrib.Service.MinimalAPIs --version 0.6.0-preview.13
  1. 註冊Masa版的MinimalAPI,修改Program
var app = builder.AddServices();
  1. 新增加一個用戶的服務,新增UserService
public class UserService : ServiceBase
{
    public IResult Add(RegisterUserRequest request)
    {
        //模擬添加用戶
        return Results.Ok();
    }
}

到這裡已經結束了,可能會有小夥伴十分的疑惑,Masa提供的方案讓我有點摸不著頭腦,但項目運行後就會發現在Swagger上多了一個服務

image.png

細心的小夥伴發現了,這個服務好像是我們新增的添加用戶服務,但鏈接地址為什麼是api/v1/Users 🤔🤔

進階

通過快速入門我們了解到如何使用MinimalAPI,但我們也清楚流水賬式編程的危害,我們不希望讓項目中充斥著流水賬式的程式碼,我們希望它是整潔的,並且是有跡可循的,這時候Masa提供的MinimalAPI方案進入了我們的視野,它上手難度極低,對我們來說它是很棒的,但如果我們不清楚它是如何設計的話,我們敢放心大膽的使用它嗎?雖然它有些枯燥,但我們必須要掌握它是如何設計的,它都支援了什麼樣的功能

約定

當服務未禁用自動映射路由時,框架會自動掃描繼承ServiceBase的非抽象子類並註冊到服務集合中(IServiceCollection),並為滿足以下要求的方法自動註冊路由

  • 當前類的方法的訪問級別為public(不包含父類方法)
  • 方法上未增加特性IgnoreRouteAttribute

路由規則

路由規則優先順序:

自定義路由 > 約定生成路由

  1. 如何自定義路由?

通過RoutePattern特性我們可以為方法自定義路由

[RoutePattern("user/add")]
public IResult Add([FromBody]RegisterUserRequest request)
{
    //模擬添加用戶
    return Results.Ok();
}
  1. 約定的生成路由規則為:

Pattern(路由) = BaseUri + RouteMethodName

  • BaseUri: 根地址,默認: null
    • BaseUri或者null時,則 BaseUri = Prefix/Version/ServiceName
  • RouteMethodName: 除非自定義RouteMethodName,否則RouteMethodName = GetMethodName(方法名)

GetMethodName:

  1. TrimStart:Get/Post/Create/Put/Update/Delete/Remove
  2. 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(根據方法名前綴自動識別),如果希望指定請求類型而非自動識別,則可手動指定:GetPostPutDelete

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提供了哪些配置,那下面就讓我們實戰來演練一下,通過模擬不同的場景使用不同的配置,以確保我們正確掌握這些知識

  1. 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);
        }
    }
    
  2. A: 我是一個新手,我覺得我的項目不需要使用前綴以及版本,我希望自動映射的路由可以幫助我刪掉它們

    Q: 你需要的是全局配置,通過全局配置禁用前綴以及版本即可,例如:

    var app = builder.AddServices(options =>
    {
        options.Prefix = string.Empty;
        options.Version = string.Empty;
    });
    
  3. 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")]

  1. 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將對當前服務失效,局部配置存在時,全局配置將不起作用

  1. A: 我希望某個服務不需要經過授權即可訪問,那我該怎麼做?
    Q: 只需要在方法上加AllowAnonymous特性即可, 它是MinimalAPI支援的,除了AllowAnonymousEnableCorsAuthorize等都是支援的, 但HttpGetHttpPostHttpPutHttpDelete特性是不支援的

    public class UserService : ServiceBase
    {
        [AllowAnonymous]
        public void CreateUser([FromBody] RegisterUserRequest request, IData data)
        {
            data.Add(request.Name, request.Age);
        }
    }
    

常見問題

  1. 為何使用DbContext時總是提示DbContext已經被釋放?

    UserService僅在項目啟動時會被初始化一次,之後不再初始化,因此Service的構造函數參數僅支援SingletonTransient。如果您的服務的生命周期為Scoped,建議在對應的方法中增加參數,例如:

    public void Add([FromBody] RegisterUserRequest request, IData data)
    {
        data.Add(request.Name, request.Age);
    }
    
  2. 模型校驗不起作用?

    目前版本的MinimalAPI並不支援模型綁定與驗證,後續版本會增加支援

  3. Builder.AddServices()又為什麼必須要放到最後?

    我們知道通過builder.Build()可以得到WebApplication,但在.Net6.0中新增加了限制,這個限制就是在Build後無法再次更新IServiceCollection,否則會提示Cannot modify ServiceCollection after application is built

  4. 為什麼MinimalAPIs的生命周期是單例?

    目前AddServices方法中做了兩件事,第一件事就是獲取到所有的服務,並註冊到服務集合中,第二件事就是觸發服務並將對應服務的地址以及方法映射到到AppApp.Map類似App.Use,也是一個擴展方法,類似MVC的路由,其生命周期是單例,我們僅僅是將繼承ServiceBase的服務映射到App中,並沒有魔改MinimalAPI,因此並不存在性能問題,但同樣其生命周期也無法改變

總結

MinimalAPIMVC我應該如何選擇?

小型服務使用MinimalAPI,因為它是很輕量級的,但如果是大型服務或者功能特別複雜的,還是推薦使用MVCMinimalAPI的上手成本很低,但它不是銀彈,選擇適合自己的才是最好的

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,歡迎聯繫我們

16373211753064.png