访问远程数据

提示

此内容摘自电子书《使用 .NET MAUI 的企业应用程序模式》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

许多现代的基于 Web 的解决方案都利用 Web 服务器托管的 Web 服务,来为远程客户端应用程序提供相关功能。 Web 服务公开的操作构成 Web API。

客户端应用应该能够在不知道 API 公开的数据或操作是如何实现的情况下利用 Web 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 请求中包含的数据和 Web 服务器返回的相应响应消息可以各种格式(称为媒体类型)提供。 当客户端应用发送需要在消息正文中返回数据的请求时,它可以在请求的 Accept 标头中指定它可以处理的媒体类型。 如果 Web 服务器支持此媒体类型,则可以使用包含 Content-Type 标头(指定消息正文中的数据的格式)的响应来应答。 然后,客户端应用负责分析响应消息并相应地解释消息正文中的结果。

有关 REST 的详细信息,请参阅 Microsoft Docs 上的 API 设计API 实现

使用 RESTful API

eShopOnContainers 多平台应用使用模型-视图-视图模型 (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 类的实例。 然后,它向由 URI 标识的资源提交异步 GET 请求,并将响应存储在 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);
}

此方法使用 EntityFramework 从 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 类的实例。 然后,它向由 URI 标识的资源提交异步 POST 请求,将序列化的购物车数据以 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 类的实例。 然后,它向由 URI 标识的资源提交异步 DELETE 请求。 有关 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 上的重试模式。

断路器模式

在某些情况下,由于预期的事件需要更长的时间才能修复,因此可能会发生故障。 这些故障包括从部分连接中断到服务完全瘫痪。 在这些情况下,应用重试不太可能成功的操作毫无意义,而是应该接受该操作已失败并相应地处理此失败。

断路器模式可以防止应用重复尝试执行可能失败的操作,同时还使应用能够检测故障是否已解决。

注意

断路器模式的目的与重试模式不同。 重试模式在预期操作将成功的情况下让应用重试操作。 断路器模式阻止应用执行很可能会失败的操作。

针对可能失败的操作,断路器充当其代理。 代理应监视最近发生的失败次数,并使用此信息来决定是允许操作继续进行,还是立即返回异常。

eShopOnContainers 多平台应用当前未实现断路器模式。 但是,eShopOnContainers 会这样做。 有关详细信息,请参阅适用于容器化 .NET 应用程序的 .NET 微服务体系结构

提示

结合重试和断路器模式。

应用可使用重试模式通过断路器调用操作来结合重试和断路器模式。 但重试逻辑应该对断路器返回的任何异常保持敏感,并且在断路器指示故障为非临时性的情况下放弃重试尝试。

有关断路器模式的详细信息,请参阅 Microsoft Docs 上的断路器模式。

总结

许多现代的基于 Web 的解决方案都利用 Web 服务器托管的 Web 服务,来为远程客户端应用程序提供相关功能。 Web 服务公开的操作构成 Web API,客户端应用应该能够在不知道 API 公开的数据或操作是如何实现的情况下利用 Web API。

可通过将经常访问的数据缓存到靠近应用的快速存储中来提高应用性能。 应用可使用缓存端模式实现通读缓存。 此模式可确定项当前是否在缓存中。 如果项不在缓存中,则会从数据存储中读取该项并将其添加到缓存中。

与 Web API 通信时,应用必须对暂时性故障敏感。 暂时性故障包括暂时断开与服务的网络连接、服务暂时不可用,或者当服务繁忙时出现超时。 这些故障通常可自我纠正,如果在适当的延迟后重复操作,那么操作可能会成功。 因此,应用应将所有访问 Web API 的尝试封装在实现瞬态故障处理机制的代码中。