使用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",以使用此选项。选择此选项后,可以看到如下图所示的第一个配置选项。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL3tGVNRTQ65EeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLxkzNyUTN1ATM4EDMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
使用这些选项,可以选择在调用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计划中。