天天看点

8. 使用 Azure Function

使用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",以使用此选项。选择此选项后,可以看到如下图所示的第一个配置选项。

8. 使用 Azure Function
8. 使用 Azure Function

使用这些选项,可以选择在调用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计划中。

继续阅读