天天看點

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計劃中。

繼續閱讀