本教學課程是系列課程的第一 部分 。 在本教學課程中,瞭解如何建立具有 ASP.NET Core Web API 前端和具狀態後端服務的 Azure Service Fabric 應用程式,以儲存您的資料。 當您完成時,您就有一個投票應用程式,其中包含一個 ASP.NET 核心 Web 前端,可將投票結果儲存在叢集中的具狀態後端服務中。
本教學課程系列需要 Windows 開發人員電腦。 如果您不想手動建立投票應用程式,您可以下載已完成應用程式的 原始程式碼 ,然後跳至 逐步解說投票範例應用程式。 您也可以檢視本教學課程的 影片逐步解說 。
在本教學課程中,您將瞭解如何:
- 將 ASP.NET Core Web API 服務建立為具狀態的可靠服務
- 將 ASP.NET 核心 Web 應用程式服務建立為無狀態 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。
選取 [ 檔案>新增>專案 ] 以建立新專案。
在 [ 建立新專案] 上,選取 [Cloud>Service Fabric 應用程式]。 選取 下一步。
針對新的專案類型選取 [無狀態 ASP.NET 核心],為您的服務命名為 [VotingWeb],然後選取 [建立]。
下一個窗格會顯示一組 ASP.NET 核心專案範本。 在本教學課程中,選取 [Web 應用程式(模型-視圖-控制器)],然後選取 [確定]。
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();
}))
};
}
然後在GetVotingDataServiceName之後新增下列CreateServiceInstanceListeners()方法,然後儲存變更。
GetVotingDataServiceName 在輪詢時傳回服務名稱。
internal static Uri GetVotingDataServiceName(ServiceContext context)
{
return new Uri($"{context.CodePackageActivationContext.ApplicationName}/VotingData");
}
新增VotesController.cs檔案
新增控制器以定義投票動作。 以滑鼠右鍵按一下 Controllers 資料夾,然後選取 [新增>項目] [>] [ASP.NET Core] [>]。 將檔案命名為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>
然後更新 Voting 專案中的 Application URL 屬性值,以確保在您偵錯應用程式時,Web 瀏覽器會開啟至正確的埠。 在 [方案總管] 中,選取 [投票] 專案,然後將 Application URL 屬性更新為 8080。
在本機部署和執行投票應用程式
您現在可以執行投票應用程式來偵錯它。 在 Visual Studio 中,選取 F5 以偵錯模式將應用程式部署至本機 Service Fabric 叢集。 如果您先前未使用 [以系統管理員身分執行] 選項開啟 Visual Studio,應用程式就會失敗。
備註
第一次在本機執行和部署應用程式時,Visual Studio 會建立本機 Service Fabric 叢集以用於偵錯。 建立叢集的程序可能需要一些時間。 叢集建立狀態會顯示在 Visual Studio 的 [輸出] 視窗中。
將投票應用程式部署至本機 Service Fabric 叢集之後,您的 Web 應用程式會自動在瀏覽器索引標籤中開啟。它看起來類似於以下示例:
若要停止偵錯應用程式,請返回 Visual Studio,然後選取 Shift+F5。
在您的應用程式中新增狀態後端服務
現在 ASP.NET Web API 服務正在應用程式中執行,請新增可設定狀態的可靠服務,以將某些資料儲存在應用程式中。
您可以使用 Service Fabric,使用可靠的集合,在服務內一致且可靠地儲存資料。 可靠集合是一組高度可用且可靠的集合類別,任何有使用 C# 集合經驗的人都熟悉這些類別。
若要建立將計數器值儲存在可靠集合中的服務:
在 [方案總管] 中,以滑鼠右鍵按一下 [投票應用程式專案中的 服務 ],然後選取 [ 新增>Service Fabric 服務]。
在 [ 新增 Service Fabric 服務 ] 對話方塊中,選取 [可設定狀態 ASP.NET 核心],將服務命名為 [VotingData],然後選取 [ 確定]。
建立服務專案之後,您的應用程式中會有兩個服務。 當您繼續建置應用程式時,您可以以相同的方式新增更多服務。 每個服務都可以獨立版本化和升級。
下一個窗格會顯示一組 ASP.NET 核心專案範本。 在本教學課程中,請選擇 API。
Visual Studio 會建立 VotingData 服務專案,並將其顯示在 [方案總管] 中:
新增VoteDataController.cs檔案
在 VotingData 專案中,以滑鼠右鍵按一下 [控制器] 資料夾,然後選取 [ 新增>專案>類別]。 將檔案命名為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')]"
}
],
若要尋找本機開發叢集中使用的反向代理埠,請檢視本機 Service Fabric 叢集描述檔中的 HttpApplicationGatewayEndpoint 元素:
- 若要開啟 Service Fabric 總管工具,請開啟瀏覽器並移至
http://localhost:19080網址。 - 選取 叢集>清單。
- 記下
HttpApplicationGatewayEndpoint元素連接埠。 預設情況下,埠為19081。 如果不是 19081,請變更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):ASP.NET Core Web 前端服務,提供網頁並公開 Web API 以與後端服務通訊。
- 後端服務 (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 控制器傳送請求:
- 首先,建構後端服務的反向代理的URL。 (1)
- 然後將 HTTP PUT 請求傳送至反向 Proxy。 (2)
- 最後,將後端服務的回應傳回給客戶端。 (3)
選取 F5 以繼續。
您現在位於後端服務的中斷點:
- 在方法的第一行中,使用
stateManager來取得或新增名為 的可靠字典counts。 (1) - 所有在可靠字典中具有值的互動都需要交易。 此
using陳述式會建立該交易。 (2) - 在交易過程中,更新投票選項的相關鍵值,然後提交操作。 當
commit方法傳回時,資料會在字典中更新。 然後,它會複製到叢集中的其他節點。 資料現在安全地儲存在叢集中,後端服務可以容錯移轉至其他節點,且資料仍可供使用。 (3)
- 在方法的第一行中,使用
選取 F5 以繼續。
若要停止偵錯工作階段,請選取 Shift+F5。
後續步驟
前進到下一個教學課程: