许多现代的基于Web的解决方案利用由Web服务器托管的Web服务来为远程客户端应用程序提供功能。 Web服务公开的操作构成Web API。
客户端应用程序应该能够在不知道API暴露的数据或操作如何实现的情况下使用Web API。 这要求API遵守通用标准,使客户端应用程序和Web服务能够同意使用哪些数据格式,以及在客户端应用程序和Web服务之间交换的数据的结构。
表征状态转移介绍
表征状态转移(REST)是基于超媒体构建分布式系统的架构风格。 REST模型的主要优点在于它基于开放标准,并不将模型的实现或访问它的客户端应用程序绑定到任何特定实现。因此,可以使用Microsoft ASP.NET Core MVC实现REST Web服务,客户端应用程序可能正在使用任何可以生成HTTP请求和解析HTTP响应的语言和工具集进行开发。
REST模型使用导航方案来表示通过网络(称为资源)的对象和服务。实施REST的系统通常使用HTTP协议来传输访问这些资源的请求。在这样的系统中,客户端应用程序以标识资源的URI的形式提交请求,以及指示要在该资源上执行的操作的HTTP方法(例如GET,POST,PUT或DELETE)。 HTTP请求的正文包含执行操作所需的任何数据。
注意:REST定义了无状态请求模型。因此,HTTP请求必须是独立的,并且可能以任何顺序发生。
来自REST请求的响应使用标准HTTP状态代码。例如,返回有效数据的请求应包括HTTP响应代码200(OK),而无法找到或删除指定资源的请求应返回包含HTTP状态代码404(未找到)的响应。
RESTful Web API公开了一组连接的资源,并提供核心操作,使应用程序能够操纵这些资源并轻松地在它们之间导航。因此,构成典型的RESTful Web API的URI面向其所暴露的数据,并使用HTTP提供的功能对该数据进行操作。
客户端应用程序在HTTP请求中包含的数据以及来自Web服务器的相应响应消息可以以各种格式呈现,称为媒体类型。当客户端应用程序发送返回消息正文中的数据的请求时,可以在请求的Accept标头中指定它可以处理的媒体类型。如果Web服务器支持这种媒体类型,它可以用包含Content-Type头的响应进行回复,该头指定消息正文中的数据格式。客户端应用程序负责解析响应消息,并将消息体中的结果适当地解释。
有关REST的更多信息,请参阅Microsoft Docs上的API设计和API实现。
使用RESTful API
eShopOnContainers移动应用程序使用Model-View-ViewModel(MVVM)模式,模式的模型元素表示应用程序中使用的域实体。 eShopOnContainers参考应用程序中的控制器和存储库类接受并返回许多这些模型对象。因此,它们被用作数据传输对象(DTO),其保存在移动应用和集装式微服务之间传递的所有数据。使用DTO将数据传递到Web服务并从Web服务接收数据的主要好处是通过在单个远程呼叫中传输更多数据,该应用程序可以减少需要进行的远程呼叫的数量。
制作Web请求
eShopOnContainers移动应用程序使用HttpClient类通过HTTP进行请求,JSON用作媒体类型。该类提供了异步发送HTTP请求并从URI标识的资源接收HTTP响应的功能。 HttpResponseMessage类表示在HTTP请求完成后从REST API接收的HTTP响应消息。它包含有关响应的信息,包括状态代码,标题和任何正文。 HttpContent类表示HTTP主体和内容头,例如Content-Type和Content-Encoding。可以使用任何ReadAs方法来读取内容,例如ReadAsStringAsync和ReadAsByteArrayAsync,这取决于数据的格式。
发出GET请求
CatalogService类用于从目录微服务器管理数据检索过程。在ViewModelLocator类中的RegisterDependencies方法中,CatalogService类是使用Autofac依赖注入容器注册为针对ICatalogService类型的类型映射。然后,当创建CatalogViewModel类的实例时,其构造函数接受一个ICatalogService类型,该类型由Autofac解析,返回一个CatalogService类的实例。有关依赖注入的更多信息,请参阅依赖注入简介。
图10-1显示了从Catalog Catalog中读取目录数据库中的目录数据的类的交互。
图10-1:从目录微服务器检索数据
当CatalogView导航到,CatalogViewModel类中的OnInitialize方法被调用。 此方法从目录微服务检索目录数据,如以下代码示例所示:
点击(此处)折叠或打开
- public override async Task InitializeAsync(object navigationData)
- {
- ...
- Products = await _productsService.GetCatalogAsync();
- ...
- }
此方法调用由Autofac注入到CatalogViewModel中的CatalogService实例的GetCatalogAsync方法。 以下代码示例显示了GetCatalogAsync方法:
点击(此处)折叠或打开
- public async TaskObservableCollectionCatalogItem>> GetCatalogAsync()
- {
- UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint);
- builder.Path = "api/v1/catalog/items";
- string uri = builder.ToString();
-
- CatalogRoot catalog = await _requestProvider.GetAsyncCatalogRoot>(uri);
- ...
- return catalog?.Data.ToObservableCollection();
- }
此方法构建标识请求将要发送到的资源的URI,并使用RequestProvider类在资源上调用GET HTTP方法,然后将结果返回到CatalogViewModel。 RequestProvider类包含以标识资源的URI形式提交请求的功能,指示要在该资源上执行的操作的HTTP方法以及包含执行操作所需的任何数据的主体。 有关RequestProvider类如何注入到CatalogService类中的信息,请参阅依赖注入简介。
以下代码示例显示RequestProvider类中的GetAsync方法:
点击(此处)折叠或打开
- public async TaskTResult> GetAsyncTResult>(string uri, string token = "")
- {
- HttpClient httpClient = CreateHttpClient(token);
- HttpResponseMessage response = await httpClient.GetAsync(uri);
-
- await HandleResponse(response);
- string serialized = await response.Content.ReadAsStringAsync();
-
- TResult result = await Task.Run(() =>
- JsonConvert.DeserializeObjectTResult>(serialized, _serializerSettings));
-
- return result;
- }
此方法调用CreateHttpClient方法,该方法返回HttpClient类的一个实例,并使用相应的头设置。 然后它向URI标识的资源提交异步GET请求,响应存储在HttpResponseMessage实例中。 然后调用HandleResponse方法,如果响应不包含成功的HTTP状态代码,则会抛出异常。 然后将响应读为字符串,从JSON转换为CatalogRoot对象,并返回到CatalogService。
CreateHttpClient方法显示在以下代码示例中:
点击(此处)折叠或打开
- private HttpClient CreateHttpClient(string token = "")
- {
- var httpClient = new HttpClient();
- httpClient.DefaultRequestHeaders.Accept.Add(
- new MediaTypeWithQualityHeaderValue("application/json"));
-
- if (!string.IsNullOrEmpty(token))
- {
- httpClient.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue("Bearer", token);
- }
- return httpClient;
- }
此方法创建HttpClient类的新实例,并将HttpClient实例所做的任何请求的Accept标头设置为application / json,这表示它希望使用JSON格式化任何响应的内容。 然后,如果将访问令牌作为参数传递给CreateHttpClient方法,则将其添加到由HttpClient实例创建的任何请求的授权头部,前缀为字符串Bearer。 有关授权的更多信息,请参阅授权。
当RequestProvider类中的GetAsync方法调用HttpClient.GetAsync时,将调用Catalog.API项目中CatalogController类中的Items方法,如以下代码示例所示:
点击(此处)折叠或打开
- [HttpGet]
- [Route("[action]")]
- public async TaskIActionResult> 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 PaginatedItemsViewModelCatalogItem>(
- pageIndex, pageSize, totalItems, itemsOnPage);
-
- return Ok(model);
- }
此方法使用EntityFramework从SQL数据库检索目录数据,并将其作为包含成功的HTTP状态代码的响应消息和JSON格式的CatalogItem实例的集合返回。
发出POST请求
BasketService类用于通过篮子微服务来管理数据检索和更新过程。 在ViewModelLocator类中的RegisterDependencies方法中,使用Autofac依赖注入容器将BasketService类注册为与IBasketService类型的类型映射。 然后,当创建一个BasketViewModel类的实例时,它的构造函数接受一个IBasketService类型,Autofac解析,返回一个BasketServiceclass的一个实例。 有关依赖注入的更多信息,请参阅依赖注入简介。
图10-2显示了将BasketView显示的篮子数据发送到篮子微服务器的类的交互。
图10-2:发送数据到篮子微服务器
当一个项目被添加到购物篮中时,将调用BasketViewModel类中的ReCalculateTotalAsync方法。 该方法更新了篮子中项目的总价值,并将篮子数据发送到篮子微服务器,如以下代码示例所示:
点击(此处)折叠或打开
- private async Task ReCalculateTotalAsync()
- {
- ...
- await _basketService.UpdateBasketAsync(new CustomerBasket
- {
- BuyerId = userInfo.UserId,
- Items = BasketItems.ToList()
- }, authToken);
- }
此方法调用由Autofac注入到BasketViewModel中的BasketService实例的UpdateBasketAsync方法。 以下方法显示UpdateBasketAsync方法:
点击(此处)折叠或打开
- public async TaskCustomerBasket> 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 TaskTResult> PostAsyncTResult>(
- string uri, TResult data, string token = "", string header = "")
- {
- HttpClient httpClient = CreateHttpClient(token);
- ...
- var content = new StringContent(JsonConvert.SerializeObject(data));
- content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
- HttpResponseMessage response = await httpClient.PostAsync(uri, content);
-
- await HandleResponse(response);
- string serialized = await response.Content.ReadAsStringAsync();
-
- TResult result = await Task.Run(() =>
- JsonConvert.DeserializeObjectTResult>(serialized, _serializerSettings));
-
- return result;
- }
此方法调用CreateHttpClient方法,该方法返回HttpClient类的一个实例,并使用相应的头设置。 然后它向URI标识的资源提交异步POST请求,序列化的数据将以JSON格式发送,响应将存储在HttpResponseMessage实例中。 然后调用HandleResponse方法,如果响应不包含成功的HTTP状态代码,则会抛出异常。 然后,响应被读取为字符串,从JSON转换为CustomerBasket对象,并返回到BasketService。 有关CreateHttpClient方法的更多信息,请参阅创建GET请求。
当RequestProvider类中的PostAsync方法调用HttpClient.PostAsync时,将调用Basket.API项目中的BasketController类中的Post方法,如以下代码示例所示:
点击(此处)折叠或打开
- [HttpPost]
- public async TaskIActionResult> Post([FromBody]CustomerBasket value)
- {
- var basket = await _repository.UpdateBasketAsync(value);
- return Ok(basket);
- }
此方法使用RedisBasketRepository类的一个实例将购物篮数据保存到Redis缓存,并将其作为包含成功HTTP状态代码的响应消息和JSON格式化的CustomerBasket实例返回。
进行删除请求
图10-3显示了对于CheckoutView,删除篮子微服务器中的篮子数据的类的交互。
图10-3:从篮子微服务器中删除数据
调用结帐过程时,将调用CheckoutViewModel类中的CheckoutAsync方法。 此方法在清除购物篮之前创建一个新订单,如以下代码示例所示:
点击(此处)折叠或打开
- private async Task CheckoutAsync()
- {
- ...
- await _basketService.ClearBasketAsync(_shippingAddress.Id.ToString(), authToken);
- ...
- }
此方法调用由Autofac注入到CheckoutViewModel中的BasketService实例的ClearBasketAsync方法。 以下方法显示ClearBasketAsync方法:
点击(此处)折叠或打开
- public async Task ClearBasketAsync(string guidUser, string token)
- {
- UriBuilder builder = new UriBuilder(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 = CreateHttpClient(token);
- await httpClient.DeleteAsync(uri);
- }
此方法调用CreateHttpClient方法,该方法返回HttpClient类的一个实例,并使用相应的头设置。 然后它向由URI标识的资源提交异步DELETE请求。 有关CreateHttpClient方法的更多信息,请参阅创建GET请求。
当RequestProvider类中的DeleteAsync方法调用HttpClient.DeleteAsync时,将调用Basket.API项目中的BasketController类中的Delete方法,如以下代码示例所示:
点击(此处)折叠或打开
- [HttpDelete("{id}")]
- public void Delete(string id)
- {
- _repository.DeleteBasketAsync(id);
- }
此方法使用RedisBasketRepository类的实例从Redis缓存中删除购物篮数据。
缓存数据
通过将经常访问的数据缓存到位于应用程序附近的快速存储,可以提高应用程序的性能。如果快速存储位置比原始源更靠近应用程序,则缓存可以显着提高检索数据时的响应时间。
最常见的缓存形式是直读缓存,应用程序通过引用缓存来检索数据。如果数据不在缓存中,则从数据存储中检索数据,并将其添加到缓存中。应用程序可以使用高速缓存备用模式实现直读缓存。此模式确定项目当前是否在缓存中。如果项目不在缓存中,则从数据存储中读取并将其添加到缓存。有关详细信息,请参阅Microsoft Docs上的Cache-Aside模式。
? 提示:缓存经常读取的数据,并且不经常更改。该数据可以在应用程序首次检索时根据需要添加到缓存中。这意味着应用程序只需要从数据存储中提取一次数据,并且可以通过使用缓存来满足后续访问。
分布式应用程序(如eShopOnContainers参考应用程序)应提供以下缓存之一或两者:
· 共享缓存,可由多个进程或计算机访问。
· 私有缓存,数据在运行该应用的设备上本地保存。
eShopOnContainers移动应用程序使用私有缓存,数据在本地运行在运行应用程序实例的设备上。有关eShopOnContainers参考应用程序使用的缓存的信息,请参阅.NET Microservices:集群化.NET应用程序的体系结构。
? 提示:将缓存视为可能随时消失的瞬态数据存储。确保数据在原始数据存储以及缓存中维护。如果缓存变得不可用,则丢失数据的机会被最小化。
管理数据到期
期望缓存的数据将始终与原始数据一致是不切实际的。原始数据存储中的数据可能会在缓存后发生更改,导致缓存的数据变得过时。因此,应用程序应实施有助于确保缓存中的数据尽可能最新的策略,但也可以检测和处理缓存中的数据变得过时时出现的情况。大多数缓存机制使缓存能够被配置为过期数据,从而减少数据可能过期的时间。
? 提示:设置配置缓存时的默认过期时间。许多高速缓存实现了到期,如果在指定的时间段内没有访问数据,则会使数据无效并将其从高速缓存中删除。但是,在选择期满时必须小心。如果数据太短,数据将会过期,缓存的好处将会减少。如果时间太长,数据风险就会变淡。因此,到期时间应与使用数据的应用程序的访问模式相匹配。
当缓存数据到期时,应将其从缓存中移除,并且应用程序必须从原始数据存储中检索数据并将其放回到缓存中。
如果允许数据保留一段时间,缓存可能会填满。因此,可能需要向缓存中添加新项目的请求来删除称为逐出的过程中的某些项目。缓存服务通常会以最近最少使用的方式驱逐数据。但是,还有其他驱逐政策,包括最近使用的和先到先得的。有关详细信息,请参阅Microsoft Docs缓存指导。
缓存图像
eShopOnContainers手机应用程序会消耗从被缓存中受益的远程产品图像。这些图像由Image控件和FFImageLoading库提供的CachedImage控件显示。
Xamarin.Forms图像控件支持缓存下载的图像。默认情况下启用缓存,并将映像本地存储24小时。此外,可以使用CacheValidity属性配置到期时间。有关更多信息,请参阅Xamarin开发人员中心下载的图像缓存。
FFImageLoading的CachedImage控件是Xamarin.Forms Image控件的替代品,提供了附加功能的附加属性。在这个功能中,控件提供可配置的缓存,同时支持错误和加载图像占位符。以下代码示例显示了eShopOnContainers移动应用程序如何在ProductTemplate中使用CachedImage控件,该控件是CatalogView中ListView控件使用的数据模板:
点击(此处)折叠或打开
- ffimageloading:CachedImage
- Grid.Row="0"
- Source="{Binding PictureUri}"
- Aspect="AspectFill">
- ffimageloading:CachedImage.LoadingPlaceholder>
- OnPlatform
- x:TypeArguments="ImageSource"
- iOS="default_product"
- Android="default_product"
- WinPhone="Assets/default_product.png"/>
- /ffimageloading:CachedImage.LoadingPlaceholder>
- ffimageloading:CachedImage.ErrorPlaceholder>
- OnPlatform
- x:TypeArguments="ImageSource"
- iOS="noimage"
- Android="noimage"
- WinPhone="Assets/noimage.png"/>
- /ffimageloading:CachedImage.ErrorPlaceholder>
- /ffimageloading:CachedImage>
CachedImage控件将LoadPlaceholder和ErrorPlaceholder属性设置为平台特定的映像。 LoadPlaceholder属性指定要检索由Source属性指定的映像时要显示的映像,如果在尝试检索Source属性指定的映像时发生错误,则ErrorPlaceholder属性将指定要显示的映像。
顾名思义,CachedImage控件将CacheDuration属性的值指定的时间高速缓存设备上的远程映像。 当此属性值未显式设置时,将应用默认值30天。
增加弹性
与远程服务和资源通信的所有应用程序必须对瞬态故障敏感。瞬态故障包括对服务的网络连接的瞬时丢失,服务的暂时不可用性,或服务繁忙时出现的超时。这些故障通常是自我纠正的,如果在适当的延迟之后重复操作,则可能会成功。
即使在所有可预见的情况下已经彻底测试,瞬态故障也会对应用程序的感知质量产生巨大的影响。为了确保与远程服务通信的应用可靠运行,必须能够执行以下所有操作:
· 发生故障时检测故障,并确定故障是否可能是短暂的。
· 如果确定故障可能是短暂的并且跟踪操作重试的次数,则重试该操作。
· 使用适当的重试策略,其中指定重试次数,每次尝试之间的延迟以及尝试失败后采取的操作。
这种瞬态故障处理可以通过在实现重试模式的代码中包装所有访问远程服务的尝试来实现。
重试模式
如果应用程序在尝试向远程服务发送请求时检测到故障,则可以通过以下任何方式处理该故障:
· 重试操作。该应用程序可以立即重试失败的请求。
· 延迟后重试操作。应用程序应该等待适当的时间,然后再重试请求。
· 取消操作。应用程序应该取消操作并报告异常。
应重新调整重试策略以符合应用程序的业务需求。例如,重要的是优化重试计数并重试间隔到正在尝试的操作。如果操作是用户交互的一部分,则重试间隔应该很短,只有少量重试尝试避免使用户等待响应。如果操作是长时间运行的工作流程的一部分,在取消或重新启动工作流程是昂贵或耗时的时候,在尝试之间等待更长时间并重试次数是合适的。
注意:在尝试之间进行最小延迟并进行大量重试的进攻性重试策略可能会降低靠近或处于容量状态的远程服务。此外,如果应用程序不断尝试执行失败操作,则此类重试策略也可能会影响应用程序的响应。
如果一个请求在一些重试后仍然失败,那么应用程序最好防止进一步的请求进入同一个资源并报告失败。然后,一段时间后,应用程序可以向资源发出一个或多个请求,看看它们是否成功。有关更多信息,请参阅断路器模式。
? 提示:永远不要实现无尽的重试机制。使用有限数量的重试,或实现断路器模式以允许服务恢复。
当进行RESTful Web请求时,eShopOnContainers手机应用程序当前不执行重试模式。但是,由FFImageLoading库提供的CachedImage控件通过重试映像加载来支持瞬态故障处理。如果图像加载失败,将进一步尝试。尝试次数由RetryCount属性指定,重试次数将在RetryDelay属性指定的延迟后发生。如果没有明确设置这些属性值,那么它们的默认值将被应用 - 3为RetryCount属性,250ms为RetryDelay属性。有关CachedImage控件的更多信息,请参阅缓存图像。
eShopOnContainers参考应用程序确实实现了重试模式。有关详细信息,包括如何将重试模式与HttpClient类组合的讨论,请参阅.NET Microservices:集群化.NET应用程序的体系结构。
有关重试模式的详细信息,请参阅Microsoft Docs上的重试模式。
断路器模式
在某些情况下,由于需要更长时间才能修复的预期事件,故障可能会发生。这些故障的范围可以从部分连接失效到服务完全失效。在这些情况下,应用程序重试不太可能成功的操作是毫无意义的,而应该接受操作失败并相应地处理此故障。
断路器模式可以防止应用程序重复尝试执行可能失败的操作,同时还允许应用程序检测故障是否已解决。
注意:断路器模式的目的不同于重试模式。重试模式使应用程序可以重试一个操作,期望它会成功。断路器图案防止应用程序执行可能失败的操作。
断路器作为可能失败的操作的代理。代理应监视最近发生的故障数量,并使用此信息来决定是否允许操作继续,或立即返回异常。
eShopOnContainers手机应用程序目前没有实现断路器模式。但是,eShopOnContainers。有关更多信息,请参阅.NET Microservices:集群化.NET应用程序的体系结构。
? 提示:组合重试和断路器模式。应用程序可以通过使用重试模式通过断路器调用操作来组合重试和断路器模式。然而,重试逻辑应对断路器返回的任何异常敏感,如果断路器指示故障不是瞬态,则放弃重试尝试。
有关断路器模式的更多信息,请参阅Microsoft Docs上的断路器模式。
概要
许多现代的基于Web的解决方案利用由Web服务器托管的Web服务来为远程客户端应用程序提供功能。 Web服务公开的操作构成Web API,并且客户端应用程序应该能够使用Web API,而不必知道API暴露的数据或操作的实现方式。
通过将经常访问的数据缓存到位于应用程序附近的快速存储,可以提高应用程序的性能。应用程序可以使用高速缓存备用模式实现直读缓存。此模式确定项目当前是否在缓存中。如果项目不在缓存中,则从数据存储中读取并将其添加到缓存。
当与Web API通信时,应用程序必须对瞬态故障敏感。瞬态故障包括对服务的网络连接的瞬时丢失,服务的暂时不可用性,或服务繁忙时出现的超时。这些故障经常是自我纠正的,如果在适当的延迟之后重复动作,那么可能会成功。因此,应用程序应该在实现一个瞬时故障处理机制的代码中包装所有访问Web API的尝试。