何为熔断降级 “熔断器如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费时间去等到长时间的超时产生。” 降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个替代响应。 简单一句话概括,降级就是在调用的下游服务A出现问题(常见超时),提供PLAN-B,返回的效果可能没有服务A好,但是聊胜于无。而熔断器的存在就是要保障何时走到降级方法,何时恢复,以什么样的策略恢复。
.NET Core 熔断降级实践 简介 Polly是一种.NET弹性和瞬态故障处理库,允许我们以非常顺畅和线程安全的方式来执诸如行重试,断路,超时,故障恢复等策略。 Polly当前版本可以工作在 .NET Standard 1.1 (包括: .NET Framework 4.5-4.6.1, .NET Core 1.0, Mono, Xamarin, UWP, WP8.1+) 和 .NET Standard 2.0+ (包括: .NET Framework 4.6.1, .NET Core 2.0+, 新版本的 Mono, Xamarin and UWP targets).上,同时也为旧版本的.NET Framework提供了一些可用的旧版本,具体版本对应如下:
目标框架 最低适配版本 最高适配版本 .NET Standard 2.1 for use with IHttpClientFactory 6.0.1 Current .NET Standard 2.0 (dedicated target) 6.0.1 Current .NET Standard 2.0 5.0.3 via .Net Standard 1.0 (upward compatible)Current .NET Standard 1.1 5.0.3 via .Net Standard 1.0 (upward compatible)Current .NET Standard 1.0 5.0.3 5.1.0 .NET Framework 4.5 1.0.0 Current (via .Net Standard);5.9.0 (as dedicated target).NET Framework 4.0 with Async support 4.2.2 5.9.0 .NET Framework 4.0 1.0.0 5.9.0 .NET Framework 3.5 1.0.0 4.3.0 Various PCL targets 2.0.0 Current (via .Net Standard);4.3.0 (as dedicated PCL target)
该项目作者现已成为.NET基金会一员,项目一直在不停迭代和更新,项目地址【https://github.com/App-vNext/Polly】 。
七种恢复策略 策略 前置条件 例 此策略解决什么问题? 重试策略(Retry) (policy family)(快速开始 ; 深入学习 ) 重试策略针对的前置条件是短暂的故障延迟且在短暂的延迟之后能够自我纠正。 “也许这只是昙花一现” 允许我们做的是能够自动配置重试机制。 断路器(Circuit-breaker) (policy family)(快速开始 ; 深入学习 ) 断路器策略针对的前置条件是当系统繁忙时,快速响应失败总比让用户一直等待更好。 保护系统故障免受过载,Polly可以帮其恢复。 “痛了,自然就会放下” “让它歇一下” 当故障超过某个预先配置的阈值时, 中断电路 (块执行) 一段时间。 超时(Timeout) (快速开始 ; 深入学习 ) 超时策略针对的前置条件是超过一定的等待时间,想要得到成功的结果是不可能的。 “你不必等待,她不会再来” 保证调用者不必等待太长时间。 隔板隔离(Bulkhead Isolation) (快速开始 ; 深入学习 ) 隔板隔离针对的前置条件是当进程出现故障时,多个失败一直在主机中对资源(例如线程/ CPU)一直占用。下游系统故障也可能导致上游失败。 这两个风险都将造成严重的后果。 “一颗老鼠屎坏了一锅汤” 将受管制的操作限制在固定的资源池中,避免其他资源受其影响。 缓存(Cache) (快速开始 ; 深入学习 ) 数据不会很频繁的进行更新,相同请求的响应是相似的。 “听说 你还会再来 我翘首以盼” 首次加载数据时将响应数据进行缓存,请求时若缓存中存在则直接从缓存中读取。 回退(Fallback) (快速开始 ; 深入学习 ) 操作将仍然失败 - 但是你可以实现准备好失败后要做的补救措施。 “你若安好,我备胎到老。” 定义失败时要返回 (或要执行的操作) 的替代值。. 策略包装(PolicyWrap) (快速开始 ; 深入学习 ) 不同的故障需要不同的策略,也就意味着弹性灵活使用组合。 “谋定而后动” 允许灵活地组合上述任何策略。
实践 故障处理(被动策略) 故障处理策略处理通过策略执行的代码所引发的特定的异常或返回结果。
第一步:指定希望处理的异常(可选-指定要处理的返回结果) 指定希望处理的异常: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Policy .Handle<HttpRequestException>() Policy .Handle<SqlException>(ex => ex.Number == 1205 ) Policy .Handle<HttpRequestException>() .Or<OperationCanceledException>() Policy .Handle<SqlException>(ex => ex.Number == 1205 ) .Or<ArgumentException>(ex => ex.ParamName == "example" ) Policy .HandleInner<HttpRequestException>() .OrInner<OperationCanceledException>(ex => ex.CancellationToken != myToken)
指定要处理的返回结果 从Polly v4.3.0起,包含返回TResult的调用的策略也可以处理TResult返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound) Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError) .OrResult(r => r.StatusCode == HttpStatusCode.BadGateway) Policy .HandleResult<HttpStatusCode>(HttpStatusCode.InternalServerError) .OrResult(HttpStatusCode.BadGateway) HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.RequestTimeout, HttpStatusCode.InternalServerError, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout }; HttpResponseMessage result = await Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) .RetryAsync(...) .ExecuteAsync( )
第二步:指定策略应如何处理这些错误 重试(Retry) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Policy .Handle<SomeExceptionType>() .Retry() Policy .Handle<SomeExceptionType>() .Retry(3 ) Policy .Handle<SomeExceptionType>() .Retry(3 , onRetry: (exception, retryCount) => { }); Policy .Handle<SomeExceptionType>() .Retry(3 , onRetry: (exception, retryCount, context) => { });
不断重试直到成功(Retry forever until succeeds) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Policy .Handle<SomeExceptionType>() .RetryForever() Policy .Handle<SomeExceptionType>() .RetryForever(onRetry: exception => { }); Policy .Handle<SomeExceptionType>() .RetryForever(onRetry: (exception, context) => { });
等待并重试(Wait and retry) WaitAndRetry策略处理HTTP状态代码429的重试后状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 Policy .Handle<SomeExceptionType>() .WaitAndRetry(new [] { TimeSpan.FromSeconds(1 ), TimeSpan.FromSeconds(2 ), TimeSpan.FromSeconds(3 ) }); Policy .Handle<SomeExceptionType>() .WaitAndRetry(new [] { TimeSpan.FromSeconds(1 ), TimeSpan.FromSeconds(2 ), TimeSpan.FromSeconds(3 ) }, (exception, timeSpan) => { }); Policy .Handle<SomeExceptionType>() .WaitAndRetry(new [] { TimeSpan.FromSeconds(1 ), TimeSpan.FromSeconds(2 ), TimeSpan.FromSeconds(3 ) }, (exception, timeSpan, context) => { }); Policy .Handle<SomeExceptionType>() .WaitAndRetry(new [] { TimeSpan.FromSeconds(1 ), TimeSpan.FromSeconds(2 ), TimeSpan.FromSeconds(3 ) }, (exception, timeSpan, retryCount, context) => { }); Policy .Handle<SomeExceptionType>() .WaitAndRetry(5 , retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)) ); Policy .Handle<SomeExceptionType>() .WaitAndRetry( 5 , retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)), (exception, timeSpan, context) => { } ); Policy .Handle<SomeExceptionType>() .WaitAndRetry( 5 , retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)), (exception, timeSpan, retryCount, context) => { } );
不断等待并重试直到成功(Wait and retry forever until succeeds) 如果所有重试都失败, 重试策略将重新引发最后一个异常返回到调用代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Policy .Handle<SomeExceptionType>() .WaitAndRetryForever(retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)) ); Policy .Handle<SomeExceptionType>() .WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)), (exception, timespan) => { }); Policy .Handle<SomeExceptionType>() .WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2 , retryAttempt)), (exception, timespan, context) => { });
断路器(Circuit-breaker) 断路器策略通过在程序出错时抛出BrokenCircuitException 来屏蔽其他异常。文档 请注意, 断路器策略将重新引发所有异常 , 甚至是已处理的异常。所以使用时通常会将重试策略 和断路器策略组合使用 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 Policy .Handle<SomeExceptionType>() .CircuitBreaker(2 , TimeSpan.FromMinutes(1 )); Action<Exception, TimeSpan> onBreak = (exception, timespan) => { ... }; Action onReset = () => { ... }; CircuitBreakerPolicy breaker = Policy .Handle<SomeExceptionType>() .CircuitBreaker(2 , TimeSpan.FromMinutes(1 ), onBreak, onReset); Action<Exception, TimeSpan, Context> onBreak = (exception, timespan, context) => { ... }; Action<Context> onReset = context => { ... }; CircuitBreakerPolicy breaker = Policy .Handle<SomeExceptionType>() .CircuitBreaker(2 , TimeSpan.FromMinutes(1 ), onBreak, onReset); CircuitState state = breaker.CircuitState; breaker.Isolate(); breaker.Reset(); ```` ##### 高级断路器(Advanced Circuit Breaker) ```csharp Policy .Handle<SomeExceptionType>() .AdvancedCircuitBreaker( failureThreshold: 0.5 , samplingDuration: TimeSpan.FromSeconds(10 ), minimumThroughput: 8 , durationOfBreak: TimeSpan.FromSeconds(30 ) );
更多相关资料请参考: 文档
有关断路器模式的更多信息, 请参见:
回退策略(Fallback) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Policy .Handle<Whatever>() .Fallback<UserAvatar>(UserAvatar.Blank) Policy .Handle<Whatever>() .Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar()) Policy .Handle<Whatever>() .Fallback<UserAvatar>(UserAvatar.Blank, onFallback: (exception, context) => { });
第三步:执行策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 var policy = Policy .Handle<SomeExceptionType>() .Retry(); policy.Execute(() => DoSomething()); var policy = Policy .Handle<SomeExceptionType>() .Retry(3 , (exception, retryCount, context) => { var methodThatRaisedException = context["methodName" ]; Log(exception, methodThatRaisedException); }); policy.Execute( () => DoSomething(), new Dictionary<string , object >() {{ "methodName" , "some method" }} ); var policy = Policy .Handle<SomeExceptionType>() .Retry(); var result = policy.Execute(() => DoSomething());var policy = Policy .Handle<SomeExceptionType>() .Retry(3 , (exception, retryCount, context) => { object methodThatRaisedException = context["methodName" ]; Log(exception, methodThatRaisedException) }); var result = policy.Execute( () => DoSomething(), new Dictionary<string , object >() {{ "methodName" , "some method" }} ); Policy .Handle<SqlException>(ex => ex.Number == 1205 ) .Or<ArgumentException>(ex => ex.ParamName == "example" ) .Retry() .Execute(() => DoSomething());
为了简单起见, 上面的示例显示了策略定义, 然后是策略执行。 但是在代码库和应用程序生命周期中, 策略定义和执行可能同样经常被分离 。 例如, 可以选择在启动时定义策略, 然后通过依赖注入 将其提供给使用点。
故障处理(主动策略) 主动策略添加了不基于当策略被引发或返回时才处理错误的弹性策略。
第一步:配置 超时(Timeout) 乐观超时(Optimistic timeout) 乐观超时通过 CancellationToken 运行 , 并假定您执行支持合作取消的委托。您必须使用 Execute/Async(...)
重载以获取 CancellationToken
, 并且执行的委托必须遵守该 CancellationToken
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Policy .Timeout(30 ) Policy .Timeout(TimeSpan.FromMilliseconds(2500 )) Policy .Timeout(() => myTimeoutProvider)) Policy .Timeout(30 , onTimeout: (context, timespan, task) => { }); Policy .Timeout(30 , onTimeout: (context, timespan, task) => { logger.Warn($"{context.PolicyKey} at {context.ExecutionKey} : execution timed out after {timespan.TotalSeconds} seconds." ); }); Policy .Timeout(30 , onTimeout: (context, timespan, task) => { task.ContinueWith(t => { if (t.IsFaulted) logger.Error($"{context.PolicyKey} at {context.ExecutionKey} : execution timed out after {timespan.TotalSeconds} seconds, with: {t.Exception} ." ); }); });
示例执行:
1 2 3 4 5 6 Policy timeoutPolicy = Policy.TimeoutAsync(30 ); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => await httpClient.GetAsync(endpoint, ct), CancellationToken.None );
悲观超时(Pessimistic timeout) 悲观超时允许调用代码 “离开” 等待执行完成的委托, 即使它不支持取消。 在同步执行中, 这是以牺牲一个额外的线程为代价的。有关更多细节, 请参见文档 。 示例执行:
1 2 3 4 5 Policy timeoutPolicy = Policy.TimeoutAsync(30 , TimeoutStrategy.Pessimistic); var response = await timeoutPolicy .ExecuteAsync( async () => await FooNotHonoringCancellationAsync(), );
超时策略在发生超时时引发 TimeoutRejectedException
。 更多详情参见文档 。
隔板(Bulkhead) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Policy .Bulkhead(12 ) Policy .Bulkhead(12 , 2 ) Policy .Bulkhead(12 , context => { }); var bulkhead = Policy.Bulkhead(12 , 2 );int freeExecutionSlots = bulkhead.BulkheadAvailableCount;int freeQueueSlots = bulkhead.QueueAvailableCount;
当隔板策略的插槽全部被正在执行的操作占满是,会引发 BulkheadRejectedException
。 更多详情参见文档 。
缓存(Cache) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var memoryCache = new MemoryCache(new MemoryCacheOptions());var memoryCacheProvider = new MemoryCacheProvider(memoryCache);var cachePolicy = Policy.Cache(memoryCacheProvider, TimeSpan.FromMinutes(5 ));var cachePolicy = Policy.Cache(memoryCacheProvider, new AbsoluteTtl(DateTimeOffset.Now.Date.AddDays(1 ));var cachePolicy = Policy.Cache(memoryCacheProvider, new SlidingTtl(TimeSpan.FromMinutes(5 ));var cachePolicy = Policy.Cache(myCacheProvider, TimeSpan.FromMinutes(5 ), (context, key, ex) => { logger.Error($"Cache provider, for key {key} , threw exception: {ex} ." ); } ); TResult result = cachePolicy.Execute(context => getFoo(), new Context("FooKey" ));
有关使用其他缓存提供程序的更丰富的选项和详细信息, 请参阅:文档
策略包装(PolicyWrap) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var policyWrap = Policy .Wrap(fallback, cache, retry, breaker, timeout, bulkhead); policyWrap.Execute(...) PolicyWrap commonResilience = Policy.Wrap(retry, breaker, timeout); Avatar avatar = Policy .Handle<Whatever>() .Fallback<Avatar>(Avatar.Blank) .Wrap(commonResilience) .Execute(() => { }); Reputation reps = Policy .Handle<Whatever>() .Fallback<Reputation>(Reputation.NotAvailable) .Wrap(commonResilience) .Execute(() => { });
更多详情参见文档
无策略(NoOp) 1 2 3 NoOpPolicy noOp = Policy.NoOp();
更多详情参见文档
第二步:执行策略 同上
执行后:捕获结果或任何最终异常 使用 ExecuteAndCapture(…) 方法可以捕获执行的结果: 这些方法返回一个执行结果实例, 该实例描述的是成功执行还是错误。
1 2 3 4 5 6 7 8 9 10 var policyResult = await Policy .Handle<HttpRequestException>() .RetryAsync() .ExecuteAndCaptureAsync(() => DoSomethingAsync());
处理返回值和 Policy<TResult>
如步骤1b 所述, 从 polly v4.3.0 开始, 策略可以组合处理返回值和异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.RequestTimeout, HttpStatusCode.InternalServerError, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout }; HttpResponseMessage result = await Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) .RetryAsync(...) .ExecuteAsync( )
要处理的异常和返回结果可以以任意顺序流畅的表达。
强类型 Policy<TResult>
配置策略 .HandleResult<TResult>(...) 或.OrResult<TResult>(...) 生成特定强类型策略 Policy<TResult>,例如 Retry<TResult>, AdvancedCircuitBreaker<TResult>
。 这些策略必须用于执行返回 TResult 的委托, 即:
Execute(Func<TResult>) (and related overloads)
ExecuteAsync(Func<CancellationToken, Task<TResult>>) (and related overloads)
ExecuteAndCapture<TResult>()
.ExecuteAndCapture(…) 在非泛型策略上返回具有属性的 PolicyResult:
1 2 3 4 policyResult.Outcome - 调用是成功还是失败 policyResult.FinalException - 最后一个异常。如果调用成功, 则捕获的最后一个异常将为 null policyResult.ExceptionType - 定义为要处理的策略的最后一个异常 (如上面的 HttpRequestException) 或未处理的异常 (如 Exception). 如果调用成功, 则为 null 。 policyResult.Result - 如果执行 func, 调用成功则返回执行结果, 否则为类型的默认值
.ExecuteAndCapture<TResult>(Func<TResult>)
在强类型策略上添加了两个属性:
1 2 policyResult.FaultType - 最终的故障是处理异常还是由策略处理的结果?如果委托执行成功, 则为 null 。 policyResult.FinalHandledResult - 处理的最终故障结果;如果调用成功将为空或类型的默认值。
Policy<TResult>
策略的状态更改事件在仅处理异常的非泛型策略中, 状态更改事件 (如 onRetry 和 onBreak ) 提供 Exception 参数。 在处理 TResult 返回值的通用性策略中, 状态更改委托是相同的, 除非它们采用 DelegateResult参数代替异常。DelegateResult具有两个属性:
Exception // 如果策略正在处理异常则为则刚刚引发异常(否则为空),Result // 如果策略正在处理结果则为刚刚引发的 TResult (否则为 default(TResult))BrokenCircuitException<TResult>
非通用的循环断路器策略在断路时抛出一个BrokenCircuitException。此 BrokenCircuitException 包含最后一个异常 (导致中断的异常) 作为 InnerException。 关于 CircuitBreakerPolicy<TResult>
策略:
由于异常而中断将引发一个 BrokenCircuitException, 并将 InnerException 设置为触发中断的异常 (如以前一样)。 由于处理结果而中断会引发 ‘BrokenCircuitException<TResult>
‘, 其 Result 属性设置为导致电路中断的结果. Policy Keys 与 Context data 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var policy = Policy .Handle<DataAccessException>() .Retry(3 , onRetry: (exception, retryCount, context) => { logger.Error($"Retry {retryCount} of {context.PolicyKey} at {context.ExecutionKey} , due to: {exception} ." ); }) .WithPolicyKey("MyDataAccessPolicy" ); var customerDetails = policy.Execute(myDelegate, new Context("GetCustomerDetails" ));var policy = Policy .Handle<DataAccessException>() .Retry(3 , onRetry: (exception, retryCount, context) => { logger.Error($"Retry {retryCount} of {context.PolicyKey} at {context.ExecutionKey} , getting {context["Type" ]} of id {context["Id" ]} , due to: {exception} ." ); }) .WithPolicyKey("MyDataAccessPolicy" ); int id = ... var customerDetails = policy.Execute(context => GetCustomer(id), new Context("GetCustomerDetails" , new Dictionary<string , object >() {{"Type" ,"Customer" },{"Id" ,id}}
更多资料参考文档
PolicyRegistry 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PolicyRegistry registry = new PolicyRegistry(); registry.Add("StandardHttpResilience" , myStandardHttpResiliencePolicy); registry["StandardHttpResilience" ] = myStandardHttpResiliencePolicy; public class MyServiceGateway { public void MyServiceGateway (..., IReadOnlyPolicyRegistry<string > registry, ... ) { ... } } registry.Get<IAsyncPolicy<HttpResponseMessage>>("StandardHttpResilience" ) .ExecuteAsync<HttpResponseMessage>(...)
策略注册表具有一系列进一步的类似字典的语义, 例如 .ContainsKey(…), .TryGet(…), .Count, .Clear(), 和 Remove(…),适用于 v5.2.0 以上版本
有关详细信息, 请参阅: 文档
.NET Core 使用Polly重试机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class PollyController : ApiController { public readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy; public PollyController () { _httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>( r => r.StatusCode == HttpStatusCode.InternalServerError) .WaitAndRetryAsync(3 , retryAttempt => TimeSpan.FromSeconds(retryAttempt)); } public async Task<IHttpActionResult> Get () { var httpClient = new HttpClient(); var requestEndpoint = "http://www.baidu.com" ; HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(() => httpClient.GetAsync(requestEndpoint)); IEnumerable<string > numbers = await httpResponse.Content.ReadAsAsync<IEnumerable<string >>(); return Ok(numbers); } }