存取遠端資料

提示

本內容節錄自《Enterprise Application Patterns Using .NET MAUI》電子書,可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

許多現代化 Web 型解決方案利用網頁伺服器代管的 Web 服務來提供遠端用戶端應用程式所需功能。 Web 服務公開的作業會構成 Web API。

用戶端應用程式應能夠利用此 Web API,而不需要知道 API 所公開資料或作業的實作方式。 若要達到這個目標,API 必須遵守促使用戶端應用程式和 Web 服務就使用之資料格式,以及用戶端應用程式和 Web 服務兩者交換之資料結構等取得共識的通用標準。

具象狀態傳輸簡介

具象狀態傳輸 (REST) 是一種架構樣式,可建置以超媒體為基礎的分散式系統。 REST 模型的主要優點是基於開放標準,而能避免模型實作或存取模型的用戶端應用程式受到任何特定實作約束。 因此,您可以使用 Microsoft ASP.NET Core 來實作 REST Web 服務,並使用任何能產生 HTTP 要求及剖析 HTTP 回應的語言和工具組來開發用戶端應用程式。

REST 模型使用瀏覽配置來透過網路呈現物件和服務 (稱為「資源」)。 實作 REST 的系統通常會使用 HTTP 通訊協定來傳送這些資源的存取要求。 在這類系統中,用戶端應用程式會以識別資源的 URI 形式提交要求,以及指出要在該資源上執行作業的 HTTP 方法 (例如 GET、POST、PUT 或 DELETE)。 HTTP 要求的本文包含執行作業所需的任何資料。

注意

REST 會定義無狀態要求模型。 因此,HTTP 要求必須獨立,且可能會依任何順序發生。

來自 REST 要求的回應會使用標準 HTTP 狀態碼。 例如,傳回有效資料的要求應包含 HTTP 回應碼 200 (OK),而找不到或無法刪除指定資源的要求應傳回包含 HTTP 狀態碼 404 (Not Found) 的回應。

RESTful Web API 會公開一組連線的資源,並提供讓應用程式操作這些資源及輕易地在之間瀏覽的核心作業。 基於這個理由,構成典型 RESTful Web API 的 URI 會以其所公開的資料為導向,並使用 HTTP 所提供的機制來操作這些資料。

用戶端應用程式加入 HTTP 要求中的資料,以及來自網頁伺服器的對應回應訊息,都能以各種不同的格式來呈現 (稱為「媒體類型」)。 當用戶端應用程式傳送會在訊息本文中傳回資料的要求時,可在要求的 Accept 標頭中指定其所能處理的媒體類型。 如果網頁伺服器支援此媒體類型,就能以包含指定訊息本文中資料格式之 Content-Type 標頭的回應來回覆。 接著用戶端應用程式必須負責剖析回應訊息,並適當地解譯訊息本文中的結果。

如需 REST 的詳細資訊,請參閱 Microsoft Docs 上的 API 設計和 API 實作

使用 RESTful API

eShopOnContainers 多平台應用程式使用 Model-View-ViewModel (MVVM) 模式,而模式的模型項目則代表應用程式中使用的定義域實體。 eShopOnContainers 參考應用程式中的控制器和存放庫類別會接受並傳回其中許多模型物件。 因此,其會作為資料傳輸物件 (DTO),以保存應用程式與容器化微服務之間傳遞的所有資料。 使用 DTO 將資料傳遞至 Web 服務並從中接收資料的主要優點是,藉由在單一遠端呼叫中傳輸更多資料,應用程式可以減少需要進行的遠端呼叫數目。

提出 Web 要求

eShopOnContainers 多平台應用程式使用 HttpClient 類別透過 HTTP 提出要求,並以 JSON 作為媒體類型。 此類別提供以非同步方式將 HTTP 要求傳送至 URI 所識別資源並從中接收 HTTP 要求的功能。 HttpResponseMessage 類別代表在提出 HTTP 要求之後,從 REST API 收到的 HTTP 回應訊息。 其中包含回應的資訊,包括狀態碼、標頭和任何本文。 HttpContent 類別代表 HTTP 本文和內容標頭 (例如 Content-Type 和 Content-Encoding)。 您可以根據資料格式,使用任何 ReadAs 方法 (例如 ReadAsStringAsyncReadAsByteArrayAsync) 讀取內容。

提出 GET 要求

CatalogService 類別可用來管理目錄微服務中的資料擷取程序。 在 MauiProgram 類別的 RegisterViewModels 方法中,會使用相依性插入容器將 CatalogService 類別註冊為 ICatalogService 類型的類型對應。 然後,在建立 CatalogViewModel 類別的執行個體時,其建構函式會接受相依性插入容器所解析的 ICatalogService type,傳回 CatalogService 類別的執行個體。 如需相依性插入的詳細資訊,請參閱相依性插入

下圖顯示從目錄微服務讀取目錄資料以供 CatalogView 顯示的類別互動。

Retrieving data from the catalog microservice.

瀏覽至 CatalogView 時,會呼叫 CatalogViewModel 類別中的 OnInitialize 方法。 此方法會從目錄微服務擷取目錄資料,如下列程式碼範例所示:

public override async Task InitializeAsync()
{
    Products = await _productsService.GetCatalogAsync();
} 

此方法會對相依性插入容器插入 CatalogViewModelCatalogService 執行個體呼叫 GetCatalogAsync 方法。 下列程式碼範例示範 GetCatalogAsync 方法:

public async Task<ObservableCollection<CatalogItem>> GetCatalogAsync()
{
    UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint);
    builder.Path = "api/v1/catalog/items";
    string uri = builder.ToString();

    CatalogRoot? catalog = await _requestProvider.GetAsync<CatalogRoot>(uri);

    return catalog?.Data;          
} 

此方法會建立 URI 以識別將收到要求的資源,並使用 RequestProvider 類別在資源上叫用 GET HTTP 方法,再將結果傳回給 CatalogViewModelRequestProvider 類別包含的功能可讓您以識別資源的 URI 形式提交要求、指出要在該資源上執行作業的 HTTP 方法,以及包含執行作業所需任何資料的本文。 如需如何將 RequestProvider 類別插入 CatalogService 類別的資訊,請參閱相依性插入

下列程式碼範例顯示 RequestProvider 類別中的 GetAsync 方法:

public async Task<TResult> GetAsync<TResult>(string uri, string token = "")
{
    HttpClient httpClient = GetOrCreateHttpClient(token);
    HttpResponseMessage response = await httpClient.GetAsync(uri);

    await HandleResponse(response);
    TResult result = await response.Content.ReadFromJsonAsync<TResult>();

    return result;
}

此方法會呼叫 GetOrCreateHttpClient 方法,再由其傳回具有適當標頭集的 HttpClient 類別執行個體。 然後會將非同步 GET 要求提交至 URI 所識別的資源,並將回應儲存在 HttpResponseMessage 執行個體中。 接著會叫用 HandleResponse 方法,如果回應未包含成功 HTTP 狀態碼,則會擲回例外狀況。 然後,回應會讀取為字串、從 JSON 轉換成 CatalogRoot 物件,再傳回給 CatalogService

GetOrCreateHttpClient 方法如下列程式碼範例所示:

private readonly Lazy<HttpClient> _httpClient =
    new Lazy<HttpClient>(
        () =>
        {
            var httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            return httpClient;
        },
        LazyThreadSafetyMode.ExecutionAndPublication);

private HttpClient GetOrCreateHttpClient(string token = "")
    {
        var httpClient = _httpClient.Value;

        if (!string.IsNullOrEmpty(token))
        {
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        else
        {
            httpClient.DefaultRequestHeaders.Authorization = null;
        }

        return httpClient;
    }

此方法可用來建立新的執行個體,或擷取快取的 HttpClient 類別執行個體,並將 HttpClient 執行個體所提交任何要求的 Accept 標頭設定為 application/json,以指出預期會使用 JSON 將任何回應的內容格式化。 然後,如果將存取權杖當作引數傳遞至 GetOrCreateHttpClient 方法,就會新增至 HttpClient 執行個體所提交任何要求的 Authorization 標頭,並在前面加上字串 Bearer。 如需授權的詳細資訊,請參閱授權

提示

強烈建議快取並重複使用 HttpClient 的執行個體,以提升應用程式效能。 為每個作業建立新的 HttpClient 可能會導致通訊端耗盡的問題。 如需詳細資訊,請參閱 Microsoft 開發人員中心上的 HttpClient 具現化

RequestProvider 類別中的 GetAsync 方法呼叫 HttpClient.GetAsync 時,會叫用 Catalog.API 專案中 CatalogController 類別的 Items 方法,如下列程式碼範例所示:

[HttpGet]
[Route("[action]")]
public async Task<IActionResult> Items(
    [FromQuery]int pageSize = 10, [FromQuery]int pageIndex = 0)
{
    var totalItems = await _catalogContext.CatalogItems
        .LongCountAsync();

    var itemsOnPage = await _catalogContext.CatalogItems
        .OrderBy(c => c.Name)
        .Skip(pageSize * pageIndex)
        .Take(pageSize)
        .ToListAsync();

    itemsOnPage = ComposePicUri(itemsOnPage);
    var model = new PaginatedItemsViewModel<CatalogItem>(
        pageIndex, pageSize, totalItems, itemsOnPage);           

    return Ok(model);
}

此方法會使用 Entity Framework 從 SQL 資料庫擷取目錄資料,並當作回應訊息傳回,其中包含成功 HTTP 狀態碼,以及 JSON 格式的 CatalogItem 執行個體的集合。

提出 POST 要求

BasketService 類別可用來管理購物籃微服務的資料擷取和更新程序。 在 MauiProgram 類別的 RegisterAppServices 方法中,會使用相依性插入容器將 BasketService 類別註冊為 IBasketService 類型的類型對應。 然後,在建立 BasketViewModel 類別的執行個體時,其建構函式會接受相依性插入容器所解析的 IBasketService 類型,傳回 BasketService 類別的執行個體。 如需相依性插入的詳細資訊,請參閱相依性插入

下圖顯示將 BasketView 所顯示購物籃資料傳送至購物籃微服務的類別互動。

Sending data to the basket microservice.

將項目新增至購物籃時,會呼叫 BasketViewModel 類別中的 ReCalculateTotalAsync 方法。 此方法會更新購物籃中項目的總計值,並將購物籃資料傳送至購物籃微服務,如下列程式碼範例所示:

private async Task ReCalculateTotalAsync()
{
    // Omitted for brevity...

    await _basketService.UpdateBasketAsync(
        new CustomerBasket
        {
            BuyerId = userInfo.UserId, 
            Items = BasketItems.ToList()
        }, 
        authToken);
}

此方法會對相依性插入容器插入 BasketViewModelBasketService 執行個體呼叫 UpdateBasketAsync 方法。 下列方法會顯示 UpdateBasketAsync 方法:

public async Task<CustomerBasket> UpdateBasketAsync(
    CustomerBasket customerBasket, string token)
{
    UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint);
    string uri = builder.ToString();
    var result = await _requestProvider.PostAsync(uri, customerBasket, token);
    return result;
}

此方法會建立 URI 以識別將收到要求的資源,並使用 RequestProvider 類別在資源上叫用 POST HTTP 方法,再將結果傳回給 BasketViewModel。 請注意,需要在驗證過程中從 IdentityServer 取得的存取權杖,才能授權對購物籃微服務提交的要求。 如需授權的詳細資訊,請參閱授權

下列程式碼範例顯示 RequestProvider 類別中的其中一個 PostAsync 方法:

public async Task<TResult> PostAsync<TResult>(
    string uri, TResult data, string token = "", string header = "")
{
    HttpClient httpClient = GetOrCreateHttpClient(token);

    var content = new StringContent(JsonSerializer.Serialize(data));
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    HttpResponseMessage response = await httpClient.PostAsync(uri, content);

    await HandleResponse(response);
    TResult result = await response.Content.ReadFromJsonAsync<TResult>();
    
    return result;
}

此方法會呼叫 GetOrCreateHttpClient 方法,再由其傳回具有適當標頭集的 HttpClient 類別執行個體。 然後會將非同步 POST 要求提交至 URI 所識別的資源,以 JSON 格式傳送序列化的購物籃資料,並將回應儲存在 HttpResponseMessage 執行個體中。 接著會叫用 HandleResponse 方法,如果回應未包含成功 HTTP 狀態碼,則會擲回例外狀況。 然後,回應會讀取為字串、從 JSON 轉換成 CustomerBasket 物件,再傳回給 BasketService。 如需 GetOrCreateHttpClient 方法的詳細資訊,請參閱提出 GET 要求

RequestProvider 類別中的 PostAsync 方法呼叫 HttpClient.PostAsync 時,會叫用 Basket.API 專案中 BasketController 類別的 Post 方法,如下列程式碼範例所示:

[HttpPost]
public async Task<IActionResult> Post([FromBody] CustomerBasket value)
{
    var basket = await _repository.UpdateBasketAsync(value);
    return Ok(basket);
} 

此方法會使用 RedisBasketRepository 類別的執行個體,將購物籃資料保存至 Redis 快取,並當作回應訊息傳回,其中包含成功 HTTP 狀態碼,以及 JSON 格式的 CustomerBasket 執行個體。

提出 DELETE 要求

下圖顯示從購物籃微服務刪除 CheckoutView 中購物籃資料的類別互動。

Deleting data from the basket microservice.

叫用結帳程序時,會呼叫 CheckoutViewModel 類別中的 CheckoutAsync 方法。 此方法會在清除購物籃之前建立新的訂單,如下列程式碼範例所示:

private async Task CheckoutAsync()
{
    // Omitted for brevity...

    await _basketService.ClearBasketAsync(
        _shippingAddress.Id.ToString(), authToken);
}

此方法會對相依性插入容器插入 CheckoutViewModelBasketService 執行個體呼叫 ClearBasketAsync 方法。 下列方法顯示 ClearBasketAsync 方法:

public async Task ClearBasketAsync(string guidUser, string token)
{
    UriBuilder builder = new(GlobalSetting.Instance.BasketEndpoint);
    builder.Path = guidUser;
    string uri = builder.ToString();
    await _requestProvider.DeleteAsync(uri, token);
}

此方法會建立 URI 以識別將收到要求的資源,並使用 RequestProvider 類別在資源上叫用 DELETE HTTP 方法。 請注意,需要在驗證過程中從 IdentityServer 取得的存取權杖,才能授權對購物籃微服務提交的要求。 如需授權的詳細資訊,請參閱授權

下列程式碼範例顯示 RequestProvider 類別中的 DeleteAsync 方法:

public async Task DeleteAsync(string uri, string token = "")
{
    HttpClient httpClient = GetOrCreateHttpClient(token);
    await httpClient.DeleteAsync(uri);
}

此方法會呼叫 GetOrCreateHttpClient 方法,再由其傳回具有適當標頭集的 HttpClient 類別執行個體。 然後會將非同步 DELETE 要求提交至 URI 所識別的資源。 如需 GetOrCreateHttpClient 方法的詳細資訊,請參閱提出 GET 要求

RequestProvider 類別中的 DeleteAsync 方法呼叫 HttpClient.DeleteAsync 時,會叫用 Basket.API 專案中 BasketController 類別的 Delete 方法,如下列程式碼範例所示:

[HttpDelete("{id}")]
public void Delete(string id) =>
    _repository.DeleteBasketAsync(id);

此方法會使用 RedisBasketRepository 類別的執行個體,從 Redis 快取刪除購物籃資料。

快取資料

藉由將經常存取的資料快取至位置接近應用程式的快速儲存體,即可改善應用程式的效能。 如果快速儲存體的位置比原始來源更接近應用程式,則快取可大幅改善擷取資料時的回應時間。

最常見的快取形式是直接讀取快取,其中應用程式會透過參考快取來擷取資料。 如果資料不在快取中,就會從資料存放區擷取,並加入快取。 應用程式可以使用另行快取模式來實作直接讀取快取。 此模式會判斷項目目前是否在快取中。 如果項目不在快取中,就會從資料存放區讀取,並新增至快取。 如需詳細資訊,請參閱 Microsoft Docs 上的另行快取模式

提示

快取經常讀取且不常變更的資料。

此資料會在應用程式第一次擷取時隨選新增至快取。 這表示應用程式僅需從資料存放區擷取一次資料,而後續存取可透過使用快取來滿足。

分散式應用程式 (例如 eShopOnContainers 參考應用程式) 應提供下列其中一或兩個快取:

  • 共用快取,可由多個處理序或電腦存取。
  • 私用快取,其中資料會保留在執行應用程式的裝置本機上。

eShopOnContainers 多平台應用程式使用私用快取,其中資料會保留在執行應用程式執行個體的裝置本機上。 如需 eShopOnContainers 參考應用程式所使用的快取資訊,請參閱 .NET 微服務:容器化 .NET 應用程式的架構

提示

請將快取視為可能會隨時消失的暫時性資料存放區。

請確定資料會同時保留在原始資料存放區及快取中。 如果快取無法使用,就能將遺失資料的機率降到最低。

管理資料到期

期望快取的資料始終與原始資料一致並不實際。 原始資料存放區中的資料可能會在快取後變更,並造成快取的資料過時。 因此,應用程式應該實作的策略是,協助確保快取中的資料盡可能保持最新,也可以偵測並處理當快取中的資料變成過時的情況。 大多數快取機制都可讓您設定快取的資料到期,因此減少資料可能過期的期間。

提示

請在設定快取時,設定預設到期時間。

許多快取實作的到期是,如果資料在指定的期間內沒有被存取過,就會讓資料無效並從快取移除。 不過,選擇到期時間時,請務必小心。 如果太短,資料將會太快到期,而使快取的優點大打折扣。 如果太長,資料就有過時的風險。 因此,到期時間應該符合使用資料之應用程式的存取模式。

當快取的資料到期時,應該從快取予以移除,且應用程式必須從原始資料存放區擷取資料,並將其放回快取中。

如果允許保留資料的時間太長,則快取也可能會填滿。 因此,將新項目加入快取的要求可能必須在稱為「收回」的程序中移除某些項目。 快取服務通常會根據「最近最少使用」原則來收回資料。 不過,還有其他驅逐原則,包括「最近使用」和「先進先出」。如需詳細資訊,請參閱 Microsoft Docs 上的快取指導

快取影像

eShopOnContainers 多平台應用程式使用遠端產品影像,以從快取中獲益。 這些影像是由影像控制顯示。 .NET MAUI 影像控制在預設啟用快取的情況下支援快取已下載的影像,並將影像儲存在本機 24 小時。 此外,還可以使用 CacheValidity 屬性來設定到期時間。 如需詳細資訊,請參閱 Microsoft 開發人員中心上的已下載的影像快取

提高復原能力

與遠端服務和資源進行通訊的所有應用程式必須能感應暫時性錯誤。 暫時性錯誤包括瞬間失去服務的網路連線、暫時無法使用服務,或當服務忙碌時逾時。 這些錯誤通常會自行修正,如果在適當的延遲後再重複此動作,可能會成功。

暫時性錯誤可能會對應用程式的認知品質造成重大影響,即使應用程式已在所有可預測狀況下經過徹底的檢查也一樣。 為了確保與遠端服務通訊的應用程式能夠可靠地運作,其必須要能執行下列所有作業:

  • 在錯誤發生時偵測到錯誤,並判斷錯誤是否可能是暫時性。
  • 如果經判定錯誤可能是暫時性,則重試作業,並追蹤作業重試的次數。
  • 使用適當的重試策略,指定重試次數、每次嘗試之間延遲的時間,以及嘗試失敗後採取的動作。

此暫時性錯誤處理可透過將存取遠端服務的所有嘗試包裝在實作重試模式的程式碼中來達成。

重試模式

如果應用程式嘗試將要求傳送至遠端服務時偵測到失敗,則可以透過下列任何方式來處理失敗:

  • 重試作業。 應用程式可以立即重試失敗的要求。
  • 延遲後重試作業。 應用程式應等待一段適當的時間後再重試要求。
  • 取消作業。 應用程式應取消作業並報告例外狀況。

重試策略應該加以調整以符合應用程式的商務需求。 例如,請務必根據要嘗試的作業來最佳化重試計數和重試間隔。 如果作業是使用者互動的一部分,則重試間隔應該很短,且只會重試幾次,以避免讓使用者等待回應。 如果作業是長期執行工作流程的一部分,由於取消或重新開始工作流程不是很昂貴就是很耗時,因此嘗試之間的等待時間適合較長,也適合重試更多次。

注意

積極的重試策略在嘗試之間有最短的延遲,而大量重試的結果,可能會讓即將達到或已達容量的遠端服務效能下降。 此外,如果持續嘗試執行失敗的作業,這類重試策略也可能會影響應用程式的回應能力。

如果在大量重試之後要求仍失敗,最好避免應用程式再向相同資源提出要求,並回報失敗。 然後,在一定時間後,應用程式可以對資源提出一或多個要求,看看是否會成功。 如需詳細資訊,請參閱斷路器模式

提示

永不實作無止盡的重試機制。 相反地,建議使用指數輪詢。

請限制重試次數,或實作斷路器模式以允許服務復原。

eShopOnContainers 參考應用程式會實作重試模式。 如需包括如何結合重試模式與 HttpClient 類別討論的詳細資訊,請參閱 .NET 微服務:容器化 .NET 應用程式的架構

如需重試模式的詳細資訊,請參閱 Microsoft Docs 上的重試模式

斷路器模式

在某些情況下,可能會因花太長時間修正的預期事件而發生錯誤。 這些錯誤可能從失去部分連線到服務完全失敗都包括在內。 在這些情況下,讓應用程式重試不太可能成功的作業是無意義的;相反地,應快速接受作業失敗,並據以處理此失敗。

斷路器模式可防止應用程式重複嘗試執行可能失敗的作業,同時讓應用程式偵測是否已解決錯誤。

注意

斷路器模式的目的與重試模式不同。 重試模式會在預期作業會成功的情況下,讓應用程式重試作業。 斷路器模式可防止應用程式執行可能失敗的作業。

斷路器可作為可能失敗之作業的 Proxy。 Proxy 應監視最近發生的失敗次數,並使用這項資訊來決定是允許作業繼續,還是立即傳回例外狀況。

eShopOnContainers 多平台應用程式目前未實作斷路器模式。 不過,eShopOnContainers 已實作。 如需詳細資訊,請參閱 .NET 微服務:容器化 .NET 應用程式的架構

提示

結合重試模式與斷路器模式。

應用程式可以使用重試模式透過斷路器來叫用作業,以結合重試模式與斷路器模式。 不過,重試邏輯應該會受到斷路器所傳回之任何例外狀況的影響,而且如果斷路器指出錯誤不是暫時的,就應該放棄重試嘗試。

如需斷路器模式的詳細資訊,請參閱 Microsoft Docs 上的斷路器模式

摘要

許多現代化 Web 型解決方案利用網頁伺服器代管的 Web 服務來提供遠端用戶端應用程式所需功能。 Web 服務所公開的作業會構成 Web API,用戶端應用程式應能夠利用此 Web API,而不需要知道 API 所公開資料或作業的實作方式。

藉由將經常存取的資料快取至位置接近應用程式的快速儲存體,即可改善應用程式的效能。 應用程式可以使用另行快取模式來實作直接讀取快取。 此模式會判斷項目目前是否在快取中。 如果項目不在快取中,就會從資料存放區讀取,並新增至快取。

使用 Web API 進行通訊時,應用程式必須能感應暫時性錯誤。 暫時性錯誤包括瞬間失去服務的網路連線、暫時無法使用服務,或當服務忙碌時逾時。 這些錯誤通常會自行修正,如果在適當的延遲後再重複此動作,則可能會成功。 因此,應用程式應該將存取 Web API 的所有嘗試包裝在實作暫時性錯誤處理機制的程式碼中。