使用ASP.NET Core建立Web API時,可以使用運作IIS的Windows伺服器、運作Apache的Linux伺服器,甚至是沒有其他Web伺服器前端的Kestrel伺服器來托管它。可以使用Platform as a Service(PaaS)産品,例如,Azure App Service來托管Web API。使用Azure App Service時,需要根據CPU核心的數量、RAM的大小和存儲大小為伺服器執行個體付費。這些資源是為Web應用程式預留的(可以在一個App Service執行個體中運作多個Web應用程式)。
根據負載,還有一個托管Web API的選項;Consumption計劃或App Service計劃。對于App Service計劃,可以在可能已經擁有的App Service中運作Azure Function。另一中變體即Consumption計劃,也稱為serverless或Function as a Service(Faas)。使用此選項,為運作Azure Function所需的請求數和記憶體付費。根據需要的資源,這個選項可能比使用App Service要便宜的多,但也可能更昂貴。還可以在App Service執行個體中運作Azure Function,該執行個體将其更改為與App Service相同的支付計劃。
以無伺服器方式使用Azure Function時,它後面仍然有一個伺服器。Azure Function技術總是基于App Service。但是,以無伺服器方式使用它時,不會控制這個伺服器,也沒有保留CPU和記憶體。這就是價格不同的原因。有關Microsoft Azute定價模型的更多資訊,請參見 https://azure.microsoft.com/pricing/ 。
使用FaaS托管Azure Function有一些限制。Azure Function最多可以運作10分鐘。預設逾時為5分鐘,但可以延長到10分鐘。如果Azure Function需要運作更長的時間,就應該在App Service計劃中托管Azure Function。
Azure Function使用靜态方法實作的。在多個調用之間共享靜态狀态。但是,當不需要Azure Function時,它就會解除安裝,當HTTP請求再次到達時,它會重新加載和執行個體化。第一個請求可能需要更長的時間來傳回結果。像App Service中always on這樣的選項是不可用于Consumption計劃的。根據負載,可以使用Azure Function自動啟動其他機器,這是Consumption計劃的另一個特性。隻需要確定在靜态類成員中的調用之間不共享狀态。可以使用外部存儲特性(如Azure Storage或SQL資料庫)進行狀态共享。
1. 建立 Azure Function
如果使用通過DI使用的服務建立Web API,并且該服務是在.NET标準庫中定義的,就可以輕松地在Azure Function中使用相同的服務。使用Visual Studio 2017+ 時,可以在Add New Project中選擇Cloud類别,并選擇Azure Function模闆,來建立Azure Function項目。需要安裝Visual Studio擴充"Azute Functions and Web Jobs Tools",以使用此選項。選擇此選項後,可以看到如下圖所示的第一個配置選項。
使用這些選項,可以選擇在調用Function時觸發器的類型。有許多不同的觸發器可用。這些觸發器的例子包括:把一些資料寫入Azure Cosmos DB、激活一個WebHook、在Microsoft Graph上發生的事件、SMS到達、到達Event Hub的事件、發生在Blob Storage中的事件等。最常用的觸發器出現在這個對話框中;這些是HTTP請求、Azure存儲隊列中的項和計時事件。對于存儲隊列,當消息到達隊列時,Function就可以啟動。有了計時器觸發器,就可以指定時間間隔,或者在特定的時間啟動Function,比如每個星期六或每個月的第一個星期一。Azure Function是在間隔時間運作所需背景功能的最好實踐——例如,清理或分析資料的存儲過程。這一章主要讨論Web API,這裡将使用HTTP觸發器——觸發接收HTTP請求的觸發器。
要選擇的另一個選項是Azure Function的版本。在Azure Functions 1.0中建立了.NET Framework庫。Azure Functions 2.0使用.NET Standard 2.0,這通常是最好的選擇(現在已經是v3版本,使用.net core 3.1)。隻需要注意什麼觸發器可用于所選的版本。在作者撰寫本文時,WebHook還不能用于Azure Functions 2.0,但可以用于Azure Functions 1.0。
還需要一個帶有Azure Function的存儲賬戶。要在本地系統上建立和測試Azure Function,可以使用存儲模拟器。在Azure Function中寫入日志資訊需要使用存儲賬戶。
有了通路權限,就指定哪些函數應該可用。可以選擇隻從其他函數中調用可通路的函數,而不從公共函數調用。這裡選擇Anonymous作為從外部通路Azure Function的通路權限。
建立這個項目時,會建立一個引用了NuGet包Microsoft.NET.Sdk.Functions的項目。該項目包含源檔案Function.cs,以及GET請求的簡單Hello, name實作。下一節将對其進行更改,以便在GET、POST、PUT和DELETE請求上調用BookChaptersService。
2. 使用依賴注入容器
雖然BookChaptersServicer很容易通過預設的構造函數來執行個體化,但是對于許多其他服務來說,這是不可能的,比如在構造函數中需要BooksContext的DbBookChaptersService。這就是為什麼添加DI容器Microsoft.Extensions.DependencyInjection NuGet包是有用的原因。
BookServiceFunction類(托管Azure Function的類)的靜态構造函數調用在DI容器中注冊服務的ConfigureService方法,使用SampleChapters類添加示例章節的FeedSampleChapters方法,以及GetRequiredService方法,在該方法中,服務将稍後由Azure Function的所有特性使用:
static BookServiceFunction()
{
ConfigureServices();
GetRequiredServices();
}
ConfigureServices方法将服務配置到DI容器中,這是在使用ASP.NET Coe時多次看到的功能:
static void ConfigureServices()
{
var service = new ServiceCollection();
service.AddSingleton<IBookChaptersService, BookChaptersService>();
service.AddSingleton<SampleChapters>();
ApplicationServices = service.BuildServiceProvider();
}
public static IServiceProvider ApplicationServices { get; private set; }
要使一些示例章節可用,但不需要建立資料庫,CreateSampleChapters方法使用BookChaptersService建立一些記憶體中的章節。在生産中使用Azure Function時,請記住不要在記憶體中共享狀态。而這裡使用它,是因為這樣更容易示範這個例子。在很短的時間内,這些資料一直存在,但是當Function的空閑時間足夠長,或者由于同時建立多個執行個體有較高的負載時,就可能會得到意想不到的結果。要使用資料庫獲得穩定的結果,隻需要将服務注冊從BookChaptersService更改為DbBookChaptetrsServcie,并添加EF Core上下文:
//static void FeedSampleChapters()
//{
// var sampleChapters = ApplicationServices.GetRequiredService<SampleChapters>();
// sampleChapters.CreateSampleChaptersData();
//}
從GET、POST、PUT和DELETE請求到Azure Function,都需要IBookChaptersService;這就是為什麼要在靜态變量中檢索和存儲此服務的原因。使用靜态構造函數調用GetRequiredServices時,每次重新開機主機時,都會調用此方法:
public static IBookChaptersService s_bookChaptersService;
static void GetRequiredServices()
{
s_bookChaptersService = ApplicationServices.GetRequiredService<IBookChaptersService>();
}
在完成Azure Function的設定之後,可以在下一節中實作主要功能。
3. 實作GET、POST、PUT和DELETE請求
Azure Function的核心是用靜态方法Run方法定義的。函數的名稱由FunctionName特定定義。參數通過觸發器的類型來區分。示例代碼使用HttpTrigger特性指定HTTP請求上的觸發器。由于這個特性,Run方法的第一個參數類型是HttpRequest。此類型包含HTTP請求的資訊,并允許發送HTTP響應。HttpTrigger特性指定在建立應用程式時指定的AuthorizationLevel,并在Azure Function應該被激活時,在其後面添加一個HTTP謂詞的可變參數清單。還可以使用參數指定此Azure Function的路由資訊。使用路由定義的參數也可以作為參數添加到Run方法中。Run方法的最後一個參數是TraceWriter。此寫入器用于将資訊記錄到建立應用程式時指定的Azure存儲賬戶中。Run方法實作後,根據接收到的HTTP方法調用DoGet、DoPost、DoPut和DoDelete方法:
[FunctionName("BookServiceFunction")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post","put","delete", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
IActionResult result = null;
switch (req.Method)
{
case "GET":
result = DoGet(req);
break;
case "POST":
result = await DoPost(req);
break;
case "PUT":
result = await DoPut(req);
break;
case "DELETE":
result = await DoDelete(req);
break;
default:
result = new BadRequestResult();
break;
}
return result;
}
通過GET請求,用戶端可以檢索所有圖書章節,或者隻檢索一個章節。如果HTTP URL包含帶有Id(/?Id=Guid)的查詢,則解析辨別符,并調用IBookChaptersService的Find方法,根據Find方法的結果,要麼傳回NotFoundResult,要麼傳回包含HTTP主題中圖書章節的OkObjectResult。如果Id不是查詢的一部分,則使用GetAll方法檢索所有圖書章節,并将結果清單放入OkObjectResult的構造函數中:
static IActionResult DoGet(HttpRequest req)
{
string id = req.Query["Id"];
if (id != null)
{
Guid guid = Guid.Parse(id);
var chapter = s_bookChaptersService.FindAsync(guid);
if (chapter == null)
{
return new NotFoundResult();
}
return new OkObjectResult(chapter);
}
else
{
var chapters = s_bookChaptersService.GetAllAsync();
return new OkObjectResult(chapters);
}
}
使用HTTP POST請求,調用DoPost方法。POST請求包括請求的HTTP主體中的新書章節。可以通過通路HttpRequest的Body屬性來檢索HTTP主體。Body屬性的類型是Stream,它可以放在StreamReader類的構造函數中。使用StreamReader,通過調用ReadToEnd檢索完整的JSON字元串。接着在Newtonsoft.Json的幫助下,将這個JSON字元串轉換為BookChapter。然後将轉換後的BookChapter傳遞給IBookChaptersService的Add方法:
static async Task<IActionResult> DoPost(HttpRequest req)
{
string json = await new StreamReader(req.Body).ReadToEndAsync();
var chapter = JsonConvert.DeserializeObject<BookChapter>(json);
await s_bookChaptersService.AddAsync(chapter);
return new OkResult();
}
更新BookChapter對象的HTTP PUT請求與以前的HTTP POST請求非常相似。這一次隻是調用IBookChaptersService的Update方法:
static async Task<IActionResult> DoPut(HttpRequest req)
{
string json = await new StreamReader(req.Body).ReadToEndAsync();
var chapter = JsonConvert.DeserializeObject<BookChapter>(json);
await s_bookChaptersService.UpdateAsync(chapter);
return new OkResult();
}
DELETE請求:
static async Task<IActionResult> DoDelete(HttpRequest req)
{
string id = req.Query["Id"];
if (id != null)
{
Guid guid = Guid.Parse(id);
var chapter = s_bookChaptersService.FindAsync(guid);
if (guid != null)
{
await s_bookChaptersService.RemoveAsync(guid);
return new OkResult();
}
return new NotFoundResult();
}
else
{
return new BadRequestResult();
}
}
有了這些,就可以使用通過ASP.NET Core提供服務時已經實作的所有功能。使用服務的一個小部分,服務不需要任何更改。接下來,運作并釋出Azure Function。
4. 運作Azure Function
在Visual Studio中運作應用程式時,一個控制台視窗顯示了Azure Function的徽标(參見下圖),并顯示了使用URL通路HTTP服務的偵聽器的輸出。現在可以使用浏覽器發出GET請求,并測試Azure Function。對于測試POST和PUT請求,可以調整先前建立的用戶端來調用Azure Function,也可以使用Postman之類的工具(https://www.getpostman.com)建立POST和PUT請求。這也是建立內建和運作內建測試的好工具。
%%%%%%
%%%%%%
@ %%%%%% @
@@ %%%%%% @@
@@@ %%%%%%%%%%% @@@
@@ %%%%%%%%%% @@
@@ %%%% @@
@@ %%% @@
@@ %% @@
%%
%
Azure Functions Core Tools (3.0.2358 Commit hash: d6d66f19ea30fda5fbfe068fc22bc126f0a74168)
Function Runtime Version: 3.0.13159.0
[10/18/2020 11:08:22 AM] FUNCTIONS_WORKER_RUNTIME set to dotnet. Skipping WorkerConfig for language:node
[10/18/2020 11:08:22 AM] Building host: startup suppressed: 'False', configuration suppressed: 'False', startup operation id: 'eb154c0d-5431-425a
-be74-113b5fc1d3a1'
[10/18/2020 11:08:22 AM] Reading host configuration file '/Users/wangxianguo/Projects/BooksServiceSample/BookChapterServiceSample/BookServiceFunction/bin/Debug/netcoreapp3.1/host.json'
[10/18/2020 11:08:22 AM] Host configuration file read:
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "version": "2.0",
[10/18/2020 11:08:22 AM] "logging": {
[10/18/2020 11:08:22 AM] "applicationInsights": {
[10/18/2020 11:08:22 AM] "samplingExcludedTypes": "Request",
[10/18/2020 11:08:22 AM] "samplingSettings": {
[10/18/2020 11:08:22 AM] "isEnabled": true
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] Reading functions metadata
[10/18/2020 11:08:22 AM] 1 functions found
[10/18/2020 11:08:22 AM] Initializing Warmup Extension.
[10/18/2020 11:08:22 AM] FUNCTIONS_WORKER_RUNTIME set to dotnet. Skipping WorkerConfig for language:node
[10/18/2020 11:08:22 AM] Initializing Host. OperationId: 'eb154c0d-5431-425a-be74-113b5fc1d3a1'.
[10/18/2020 11:08:22 AM] Host initialization: ConsecutiveErrors=0, StartupCount=1, OperationId=eb154c0d-5431-425a-be74-113b5fc1d3a1
[10/18/2020 11:08:22 AM] LoggerFilterOptions
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "MinLevel": "None",
[10/18/2020 11:08:22 AM] "Rules": [
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "ProviderName": null,
[10/18/2020 11:08:22 AM] "CategoryName": null,
[10/18/2020 11:08:22 AM] "LogLevel": null,
[10/18/2020 11:08:22 AM] "Filter": "<AddFilter>b__0"
[10/18/2020 11:08:22 AM] },
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[10/18/2020 11:08:22 AM] "CategoryName": null,
[10/18/2020 11:08:22 AM] "LogLevel": "None",
[10/18/2020 11:08:22 AM] "Filter": null
[10/18/2020 11:08:22 AM] },
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[10/18/2020 11:08:22 AM] "CategoryName": null,
[10/18/2020 11:08:22 AM] "LogLevel": null,
[10/18/2020 11:08:22 AM] "Filter": "<AddFilter>b__0"
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] ]
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] FunctionResultAggregatorOptions
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "BatchSize": 1000,
[10/18/2020 11:08:22 AM] "FlushTimeout": "00:00:30",
[10/18/2020 11:08:22 AM] "IsEnabled": true
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] SingletonOptions
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "LockPeriod": "00:00:15",
[10/18/2020 11:08:22 AM] "ListenerLockPeriod": "00:00:15",
[10/18/2020 11:08:22 AM] "LockAcquisitionTimeout": "10675199.02:48:05.4775807",
[10/18/2020 11:08:22 AM] "LockAcquisitionPollingInterval": "00:00:05",
[10/18/2020 11:08:22 AM] "ListenerLockRecoveryPollingInterval": "00:01:00"
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] HttpOptions
[10/18/2020 11:08:22 AM] {
[10/18/2020 11:08:22 AM] "DynamicThrottlesEnabled": false,
[10/18/2020 11:08:22 AM] "MaxConcurrentRequests": -1,
[10/18/2020 11:08:22 AM] "MaxOutstandingRequests": -1,
[10/18/2020 11:08:22 AM] "RoutePrefix": "api"
[10/18/2020 11:08:22 AM] }
[10/18/2020 11:08:22 AM] Starting JobHost
[10/18/2020 11:08:22 AM] Starting Host (HostId=192-1186352543, InstanceId=48a346f2-5689-4848-8cbc-1c02e9aeb77e, Version=3.0.13159.0, ProcessId=13859, AppDomainId=1, InDebugMode=False, InDiagnosticMode=False, FunctionsExtensionVersion=(null))
[10/18/2020 11:08:22 AM] Loading functions metadata
[10/18/2020 11:08:22 AM] 1 functions loaded
[10/18/2020 11:08:23 AM] Generating 1 job function(s)
[10/18/2020 11:08:23 AM] Found the following functions:
[10/18/2020 11:08:23 AM] BookServiceFunction.BookServiceFunction.Run
[10/18/2020 11:08:23 AM]
[10/18/2020 11:08:23 AM] Initializing function HTTP routes
[10/18/2020 11:08:23 AM] Mapped function route 'api/BookServiceFunction' [get,post,put,delete] to 'BookServiceFunction'
[10/18/2020 11:08:23 AM]
[10/18/2020 11:08:23 AM] Host initialized (233ms)
[10/18/2020 11:08:23 AM] Host started (242ms)
[10/18/2020 11:08:23 AM] Job host started
Hosting environment: Production
Content root path: /Users/wangxianguo/Projects/BooksServiceSample/BookChapterServiceSample/BookServiceFunction/bin/Debug/netcoreapp3.1
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.
Http Functions:
BookServiceFunction: [GET,POST,PUT,DELETE] http://localhost:7071/api/BookServiceFunction
運作應用程式時,會發現bin/Debug/netstandard3.1/BookFunction目錄下的檔案function.json。此檔案描述了在Microsoft Azure上釋出時部署的Azure Function。使用.NET标準庫時,function.json的資訊來自使用Run方法指定的注釋;可以看出,它列出了觸發器的類型、觸發器的配置,如HTTP方法和Azure Function的入口點:
{
"generatedBy": "Microsoft.NET.Sdk.Functions-3.0.9",
"configurationSource": "attributes",
"bindings": [
{
"type": "httpTrigger",
"methods": [
"get",
"post",
"put",
"delete"
],
"authLevel": "anonymous",
"name": "req"
}
],
"disabled": false,
"scriptFile": "../bin/BookServiceFunction.dll",
"entryPoint": "BookServiceFunction.BookServiceFunction.Run"
}
成功地在本地運作應用程式後,就可以将其釋出到Microsoft Azure,或者釋出到Consumption或App Service計劃中。