教學課程:建置 .NET Service Fabric 應用程式
本教學課程是 系列的第一 部分。 在本教學課程中,瞭解如何建立具有 ASP.NET Core Web API 前端和具狀態後端服務來儲存數據的 Azure Service Fabric 應用程式。 完成時,您有一個投票應用程式,其具有 ASP.NET Core Web 前端,可將投票結果儲存在叢集中具狀態後端服務中。
本教學課程系列需要 Windows 開發人員計算機。 如果您不需要以手動建立 Voting 應用程式,可以下載已完成應用程式的原始程式碼並直接前往逐步解說投票範例應用程式。 您也可以觀看 本教學課程的影片逐步解說 。
在本教學課程中,您會了解如何:
- 建立 ASP.NET Core Web API 服務成為具狀態可靠服務
- 建立 ASP.NET Core Web 應用程式服務成為具狀態可靠服務
- 使用反向 Proxy 與具狀態服務通訊
本教學課程系列說明如何:
- 建置 .NET Service Fabric 應用程式 (本教學課程)
- 將應用程式部署到遠端叢集
- 將 HTTPS 端點新增至 ASP.NET Core 前端服務
- 使用 Azure Pipelines 設定 CI/CD
- 設定應用程式的監視和診斷
必要條件
開始進行本教學課程之前:
- 如果您沒有 Azure 訂閱,請建立免費帳戶。
- 安裝 Visual Studio 2019 15.5 版或更新版本,包括 Azure 開發工作負載和 ASP.NET 和 Web 開發工作負載。
- 安裝 Service Fabric SDK。
建立 ASP.NET Web API 服務成為可靠的服務
首先,使用 ASP.NET Core 建立投票應用程式的 Web 前端。 ASP.NET Core 是輕量型、跨平台的 Web 開發架構,可供您用來建立新式 Web UI 和 Web API。
若要完整瞭解 ASP.NET Core 如何與 Service Fabric 整合,強烈建議您在 Service Fabric Reliable Services 中檢閱 ASP.NET Core。 現在,您可以依照本教學課程來快速上手。 若要深入瞭解 ASP.NET Core,請參閱 ASP.NET Core 檔。
若要建立服務:
使用 [ 以系統管理員 身分執行] 選項開啟 Visual Studio。
選取 [檔案>新>專案] 以建立新的專案。
在 [建立新專案] 上,選取 [雲端>Service Fabric 應用程式]。 選取 [下一步]。
針對新的專案類型,選取 [無狀態 ASP.NET Core ],為您的服務 命名 VotingWeb,然後選取 [ 建立]。
下一個窗格會顯示一組 ASP.NET Core 專案範本。 在本教學課程中,選取 [Web 應用程式] (Model-View-Controller),然後選取 [ 確定]。
Visual Studio 會建立應用程式和服務項目,然後在 Visual Studio 方案總管 中顯示它們:
更新 site.js 檔案
移至 wwwroot/js/site.js 並開啟檔案。 將檔案內容取代為 [常用] 檢視所使用的下列 JavaScript,然後儲存您的變更。
var app = angular.module('VotingApp', ['ui.bootstrap']);
app.run(function () { });
app.controller('VotingAppController', ['$rootScope', '$scope', '$http', '$timeout', function ($rootScope, $scope, $http, $timeout) {
$scope.refresh = function () {
$http.get('api/Votes?c=' + new Date().getTime())
.then(function (data, status) {
$scope.votes = data;
}, function (data, status) {
$scope.votes = undefined;
});
};
$scope.remove = function (item) {
$http.delete('api/Votes/' + item)
.then(function (data, status) {
$scope.refresh();
})
};
$scope.add = function (item) {
var fd = new FormData();
fd.append('item', item);
$http.put('api/Votes/' + item, fd, {
transformRequest: angular.identity,
headers: { 'Content-Type': undefined }
})
.then(function (data, status) {
$scope.refresh();
$scope.item = undefined;
})
};
}]);
更新 Index.cshtml 檔案
移至 Views/Home/Index.cshtml ,然後開啟檔案。 此檔案具有 Home 控制器特有的檢視。 使用下列程式代碼取代其內容,然後儲存變更。
@{
ViewData["Title"] = "Service Fabric Voting Sample";
}
<div ng-controller="VotingAppController" ng-init="refresh()">
<div class="container-fluid">
<div class="row">
<div class="col-xs-8 col-xs-offset-2 text-center">
<h2>Service Fabric Voting Sample</h2>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<form class="col-xs-12 center-block">
<div class="col-xs-6 form-group">
<input id="txtAdd" type="text" class="form-control" placeholder="Add voting option" ng-model="item"/>
</div>
<button id="btnAdd" class="btn btn-default" ng-click="add(item)">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add
</button>
</form>
</div>
</div>
<hr/>
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<div class="row">
<div class="col-xs-4">
Click to vote
</div>
</div>
<div class="row top-buffer" ng-repeat="vote in votes.data">
<div class="col-xs-8">
<button class="btn btn-success text-left btn-block" ng-click="add(vote.Key)">
<span class="pull-left">
{{vote.key}}
</span>
<span class="badge pull-right">
{{vote.value}} Votes
</span>
</button>
</div>
<div class="col-xs-4">
<button class="btn btn-danger pull-right btn-block" ng-click="remove(vote.Key)">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</div>
更新 _Layout.cshtml 檔案
移至 Views/Shared/_Layout.cshtml 並開啟檔案。 此檔案具有 ASP.NET 應用程式的預設版面配置。 使用下列程式代碼取代其內容,然後儲存變更。
<!DOCTYPE html>
<html ng-app="VotingApp" xmlns:ng="https://angularjs.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@ViewData["Title"]</title>
<link href="~/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/>
<link href="~/css/site.css" rel="stylesheet"/>
</head>
<body>
<div class="container body-content">
@RenderBody()
</div>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.2/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/2.5.0/ui-bootstrap-tpls.js"></script>
<script src="~/js/site.js"></script>
@RenderSection("Scripts", required: false)
</body>
</html>
更新 VotingWeb.cs 檔案
開啟VotingWeb.cs檔案。 此檔案會使用 WebListener Web 伺服器,在無狀態服務內建立 ASP.NET Core WebHost。
在檔案的開頭,新增 using System.Net.Http;
指示詞。
以下列程序代碼取代 函 CreateServiceInstanceListeners()
式,然後儲存變更。
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
return new ServiceInstanceListener[]
{
new ServiceInstanceListener(
serviceContext =>
new KestrelCommunicationListener(
serviceContext,
"ServiceEndpoint",
(url, listener) =>
{
ServiceEventSource.Current.ServiceMessage(serviceContext, $"Starting Kestrel on {url}");
return new WebHostBuilder()
.UseKestrel()
.ConfigureServices(
services => services
.AddSingleton<HttpClient>(new HttpClient())
.AddSingleton<FabricClient>(new FabricClient())
.AddSingleton<StatelessServiceContext>(serviceContext))
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.None)
.UseUrls(url)
.Build();
}))
};
}
然後在 之後CreateServiceInstanceListeners()
新增下列GetVotingDataServiceName
方法,然後儲存您的變更。 GetVotingDataServiceName
會在輪詢時傳回服務名稱。
internal static Uri GetVotingDataServiceName(ServiceContext context)
{
return new Uri($"{context.CodePackageActivationContext.ApplicationName}/VotingData");
}
新增 VotesController.cs 檔案
新增控制器以定義投票動作。 以滑鼠右鍵按兩下 Controllers 資料夾,然後選取 [新增>專案>Visual C#>] ASP.NET [核心>類別]。 將檔案 命名VotesController.cs,然後選取 [ 新增]。
使用 下列程式 代碼取代VotesController.cs檔案內容,然後儲存變更。 稍後,在更新 VotesController.cs 檔案中,這個檔案會經過修改,以從後端服務讀取和寫入投票資料。 現在,控制器會將靜態字串資料傳回至檢視。
namespace VotingWeb.Controllers
{
using System;
using System.Collections.Generic;
using System.Fabric;
using System.Fabric.Query;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
[Produces("application/json")]
[Route("api/Votes")]
public class VotesController : Controller
{
private readonly HttpClient httpClient;
public VotesController(HttpClient httpClient)
{
this.httpClient = httpClient;
}
// GET: api/Votes
[HttpGet]
public async Task<IActionResult> Get()
{
List<KeyValuePair<string, int>> votes= new List<KeyValuePair<string, int>>();
votes.Add(new KeyValuePair<string, int>("Pizza", 3));
votes.Add(new KeyValuePair<string, int>("Ice cream", 4));
return Json(votes);
}
}
}
設定接聽連接埠
建立 VotingWeb 前端服務時,Visual Studio 會隨機選擇服務要接聽的連接埠。 VotingWeb 服務會作為此應用程式的前端,並接受外部流量。 在本節中,您會將該服務系結至固定且已知的埠。 服務資訊清單會宣告服務端點。
在 [方案總管] 中,開啟 VotingWeb/PackageRoot/ServiceManifest.xml。 在區 Resources
段中,尋找 Endpoint
項目,然後將值變更 Port
為 8080
。
若要在本機上部署並執行應用程式,則必須在您的電腦上開啟應用程式接聽連接埠,並讓此連接埠可供使用。
<Resources>
<Endpoints>
<!-- This endpoint is used by the communication listener to obtain the port on which to
listen. Please note that if your service is partitioned, this port is shared with
replicas of different partitions that are placed in your code. -->
<Endpoint Protocol="http" Name="ServiceEndpoint" Type="Input" Port="8080" />
</Endpoints>
</Resources>
然後更新 Application URL
Voting 專案中的 屬性值,以便在偵錯應用程式時,網頁瀏覽器開啟至正確的埠。 在 方案總管 中,選取 Voting 專案,然後將 屬性更新Application URL
為 8080
。
在本機部署和執行 Voting 應用程式
您現在可以執行 Voting 應用程式進行偵錯。 在 Visual Studio 中,選取 F5,以偵錯模式將應用程式部署至本機 Service Fabric 叢集。 如果您先前未使用 [ 以系統管理員 身分執行] 選項開啟 Visual Studio,應用程式就會失敗。
注意
第一次在本機執行和部署應用程式時,Visual Studio 會建立本機 Service Fabric 叢集以用於偵錯。 建立叢集的程式可能需要一些時間。 叢集建立狀態會顯示在 Visual Studio [輸出] 視窗中。
將 Voting 應用程式部署至本機 Service Fabric 叢集之後,您的 Web 應用程式會自動在瀏覽器索引標籤中開啟。看起來類似下列範例:
若要停止對應用程式進行偵錯,請返回 Visual Studio 並選取 Shift+F5。
將具狀態後端服務新增到應用程式
既然 ASP.NET Web API 服務正在應用程式中執行,請新增可設定狀態的可靠服務,以將某些資料儲存在應用程式中。
您可以使用 Service Fabric,使用可靠的集合,以一致且可靠地將數據儲存在服務內部。 可靠的集合是一組高可用性且可靠的集合類別,對於任何有使用 C# 集合經驗的人來說都是熟悉的。
若要建立將計數器值儲存在可靠集合中的服務:
在 [方案總管] 中,以滑鼠右鍵按兩下 [投票] 應用程式專案中的服務,然後選取 [新增>Service Fabric 服務]。
在 [新增 Service Fabric 服務] 對話框中,選取 [具狀態 ASP.NET 核心],將服務命名為 VotingData,然後選取 [確定]。
建立服務項目之後,您的應用程式中有兩個服務。 當您繼續建置應用程式時,可以以相同方式新增更多服務。 每個服務都可以獨立進行版本設定和升級。
下一個窗格會顯示一組 ASP.NET Core 專案範本。 在本教學課程中,選取 [API]。
Visual Studio 會建立 VotingData 服務專案,並將其顯示在 方案總管:
新增 VoteDataController.cs 檔案
在 VotingData 專案中,以滑鼠右鍵按兩下 Controllers 資料夾,然後選取[新增>項目>類別]。 將檔案 命名為VoteDataController.cs ,然後選取 [ 新增]。 以下列程式代碼取代檔案內容,然後儲存變更。
namespace VotingData.Controllers
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.ServiceFabric.Data;
using Microsoft.ServiceFabric.Data.Collections;
[Route("api/[controller]")]
public class VoteDataController : Controller
{
private readonly IReliableStateManager stateManager;
public VoteDataController(IReliableStateManager stateManager)
{
this.stateManager = stateManager;
}
// GET api/VoteData
[HttpGet]
public async Task<IActionResult> Get()
{
CancellationToken ct = new CancellationToken();
IReliableDictionary<string, int> votesDictionary = await this.stateManager.GetOrAddAsync<IReliableDictionary<string, int>>("counts");
using (ITransaction tx = this.stateManager.CreateTransaction())
{
Microsoft.ServiceFabric.Data.IAsyncEnumerable<KeyValuePair<string, int>> list = await votesDictionary.CreateEnumerableAsync(tx);
Microsoft.ServiceFabric.Data.IAsyncEnumerator<KeyValuePair<string, int>> enumerator = list.GetAsyncEnumerator();
List<KeyValuePair<string, int>> result = new List<KeyValuePair<string, int>>();
while (await enumerator.MoveNextAsync(ct))
{
result.Add(enumerator.Current);
}
return this.Json(result);
}
}
// PUT api/VoteData/name
[HttpPut("{name}")]
public async Task<IActionResult> Put(string name)
{
IReliableDictionary<string, int> votesDictionary = await this.stateManager.GetOrAddAsync<IReliableDictionary<string, int>>("counts");
using (ITransaction tx = this.stateManager.CreateTransaction())
{
await votesDictionary.AddOrUpdateAsync(tx, name, 1, (key, oldvalue) => oldvalue + 1);
await tx.CommitAsync();
}
return new OkResult();
}
// DELETE api/VoteData/name
[HttpDelete("{name}")]
public async Task<IActionResult> Delete(string name)
{
IReliableDictionary<string, int> votesDictionary = await this.stateManager.GetOrAddAsync<IReliableDictionary<string, int>>("counts");
using (ITransaction tx = this.stateManager.CreateTransaction())
{
if (await votesDictionary.ContainsKeyAsync(tx, name))
{
await votesDictionary.TryRemoveAsync(tx, name);
await tx.CommitAsync();
return new OkResult();
}
else
{
return new NotFoundResult();
}
}
}
}
}
連接服務
在本節中,您會連線兩個服務。 您會讓前端 Web 應用程式從後端服務取得投票資訊,然後在應用程式中設定資訊。
Service Fabric 可讓您以與可靠服務通訊的方式提供完整的彈性。 在單一應用程式中,您可能會有可透過 TCP/IP、透過 HTTP REST API 或透過 WebSocket 通訊協定存取的服務。 如需可用選項及其取捨的背景,請參閱 與服務通訊。
本教學課程使用 ASP.NET Core Web API 和 Service Fabric 反向 Proxy ,讓 VotingWeb 前端 Web 服務可以與後端 VotingData 服務通訊。 反向 Proxy 預設會設定為使用埠 19081。 反向 Proxy 埠是在設定叢集的 Azure Resource Manager 範本中設定。 若要尋找使用哪一個埠,請查看資源中的 Microsoft.ServiceFabric/clusters
叢集範本:
"nodeTypes": [
{
...
"httpGatewayEndpointPort": "[variables('nt0fabricHttpGatewayPort')]",
"isPrimary": true,
"vmInstanceCount": "[parameters('nt0InstanceCount')]",
"reverseProxyEndpointPort": "[parameters('SFReverseProxyPort')]"
}
],
若要尋找本機開發叢集中所使用的反向 Proxy 埠,請檢視 HttpApplicationGatewayEndpoint
本機 Service Fabric 叢集指令清單中的 專案:
- 若要開啟 Service Fabric Explorer 工具,請開啟瀏覽器並移至
http://localhost:19080
。 - 選取 [叢集>指令清單]。
- 記下
HttpApplicationGatewayEndpoint
專案埠。 根據預設,埠為19081。 如果不是 19081,請變更VotesController.cs程式代碼方法中的GetProxyAddress
埠,如下一節所述。
更新 VotesController.cs 檔案
在 VotingWeb 專案中,開啟 Controllers/VotesController.cs 檔案。 以 VotesController
下列程式代碼取代類別定義內容,然後儲存變更。 如果您在反覆步驟中探索到的反向 Proxy 埠不是 19081,請將 方法中的 GetProxyAddress
埠從 19081
變更為您探索到的埠。
public class VotesController : Controller
{
private readonly HttpClient httpClient;
private readonly FabricClient fabricClient;
private readonly StatelessServiceContext serviceContext;
public VotesController(HttpClient httpClient, StatelessServiceContext context, FabricClient fabricClient)
{
this.fabricClient = fabricClient;
this.httpClient = httpClient;
this.serviceContext = context;
}
// GET: api/Votes
[HttpGet("")]
public async Task<IActionResult> Get()
{
Uri serviceName = VotingWeb.GetVotingDataServiceName(this.serviceContext);
Uri proxyAddress = this.GetProxyAddress(serviceName);
ServicePartitionList partitions = await this.fabricClient.QueryManager.GetPartitionListAsync(serviceName);
List<KeyValuePair<string, int>> result = new List<KeyValuePair<string, int>>();
foreach (Partition partition in partitions)
{
string proxyUrl =
$"{proxyAddress}/api/VoteData?PartitionKey={((Int64RangePartitionInformation) partition.PartitionInformation).LowKey}&PartitionKind=Int64Range";
using (HttpResponseMessage response = await this.httpClient.GetAsync(proxyUrl))
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
continue;
}
result.AddRange(JsonConvert.DeserializeObject<List<KeyValuePair<string, int>>>(await response.Content.ReadAsStringAsync()));
}
}
return this.Json(result);
}
// PUT: api/Votes/name
[HttpPut("{name}")]
public async Task<IActionResult> Put(string name)
{
Uri serviceName = VotingWeb.GetVotingDataServiceName(this.serviceContext);
Uri proxyAddress = this.GetProxyAddress(serviceName);
long partitionKey = this.GetPartitionKey(name);
string proxyUrl = $"{proxyAddress}/api/VoteData/{name}?PartitionKey={partitionKey}&PartitionKind=Int64Range";
StringContent putContent = new StringContent($"{{ 'name' : '{name}' }}", Encoding.UTF8, "application/json");
putContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using (HttpResponseMessage response = await this.httpClient.PutAsync(proxyUrl, putContent))
{
return new ContentResult()
{
StatusCode = (int) response.StatusCode,
Content = await response.Content.ReadAsStringAsync()
};
}
}
// DELETE: api/Votes/name
[HttpDelete("{name}")]
public async Task<IActionResult> Delete(string name)
{
Uri serviceName = VotingWeb.GetVotingDataServiceName(this.serviceContext);
Uri proxyAddress = this.GetProxyAddress(serviceName);
long partitionKey = this.GetPartitionKey(name);
string proxyUrl = $"{proxyAddress}/api/VoteData/{name}?PartitionKey={partitionKey}&PartitionKind=Int64Range";
using (HttpResponseMessage response = await this.httpClient.DeleteAsync(proxyUrl))
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return this.StatusCode((int) response.StatusCode);
}
}
return new OkResult();
}
/// <summary>
/// Constructs a reverse proxy URL for a given service.
/// Example: http://localhost:19081/VotingApplication/VotingData/
/// </summary>
/// <param name="serviceName"></param>
/// <returns></returns>
private Uri GetProxyAddress(Uri serviceName)
{
return new Uri($"http://localhost:19081{serviceName.AbsolutePath}");
}
/// <summary>
/// Creates a partition key from the given name.
/// Uses the zero-based numeric position in the alphabet of the first letter of the name (0-25).
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
private long GetPartitionKey(string name)
{
return Char.ToUpper(name.First()) - 'A';
}
}
逐步解說投票範例應用程式
投票應用程式包含兩個服務:
- Web 前端服務 (VotingWeb):提供網頁並公開 Web API 以與後端服務通訊的 ASP.NET Core Web 前端服務。
- 後端服務 (VotingData):ASP.NET Core Web 服務,會公開 API 以將投票結果儲存在磁碟上保存的可靠字典中。
當您在應用程式中投票時,會發生下列事件:
JavaScript 檔案會將投票要求傳送至 Web 前端服務中的 Web API 做為 HTTP PUT 要求。
Web 前端服務使用 Proxy 來尋找 HTTP PUT 要求,並將其轉送至後端服務。
後端服務會接受連入要求,並將更新的結果儲存在可靠的字典中。 字典會復寫至叢集中的多個節點,並保存在磁碟上。 應用程式的所有資料都會儲存在叢集中,因此不需要資料庫。
在 Visual Studio 中偵錯
當您在 Visual Studio 中偵錯應用程式時,請使用本機 Service Fabric 開發叢集。 您可以根據自己的情況調整偵錯體驗。
在此應用程式中,使用可靠的字典,將數據儲存在後端服務中。 當您停止調試程式時,Visual Studio 預設會移除應用程式。 移除應用程式也會導致移除後端服務中的資料。 若要保存偵錯工作階段之間的資料,您可以在 Visual Studio 中,將 [應用程式偵錯模式] 當做 [投票] 專案上的屬性來變更。
若要查看程式代碼中會發生什麼情況,請完成下列步驟:
開啟 VotingWeb\VotesController.cs 檔案,並在 Web API
Put
的 方法中設定斷點(第 72 行)。開啟 VotingData\VoteDataController.cs 檔案,並在此 Web API
Put
的 方法中設定斷點(第 54 行)。選取 F5 以偵錯模式啟動應用程式。
返回瀏覽器並選取投票選項,或新增投票選項。 您已在 Web 前端的 API 控制器中叫用第一個斷點。
瀏覽器中的 JavaScript 會將要求傳送至前端服務中的 Web API 控制器:
- 首先,為後端服務建構反向 Proxy 的 URL。 (1)
- 然後將 HTTP PUT 要求傳送至反向 Proxy。 (2)
- 最後,將後端服務的回應傳回至用戶端。 (3)
選取 F5 以繼續。
您現在位於後端服務的斷點:
- 在方法的第一行中,使用
stateManager
來取得或新增名為counts
的可靠字典。 (1) - 在可靠字典中具有值的所有互動都需要交易。 此
using
語句會建立該交易。 (2) - 在交易中,更新投票選項的相關索引鍵值,並認可作業。
commit
方法傳回時,會在字典中更新數據。 然後,它會復寫至叢集中的其他節點。 數據現在安全地儲存在叢集中,後端服務可以故障轉移至其他節點,但仍有可用的數據。 (3)
- 在方法的第一行中,使用
選取 F5 以繼續。
若要停止偵錯會話,請選取 Shift+F5。
後續步驟
前進到下一個教學課程: