17 using System.Collections.Generic;
21 using System.Net.Http;
22 using Newtonsoft.Json;
23 using Newtonsoft.Json.Linq;
25 using RestSharp.Extensions;
35 using System.Threading;
36 using System.Net.Http.Headers;
37 using System.Collections.Concurrent;
39 using Newtonsoft.Json.Serialization;
48 private readonly BlockingCollection<Lazy<HttpClient>> _clientPool;
49 private string _dataFolder;
56 ContractResolver =
new DefaultContractResolver
58 NamingStrategy =
new CamelCaseNamingStrategy
60 ProcessDictionaryKeys =
false,
61 OverrideSpecifiedNames =
true
76 _clientPool =
new BlockingCollection<Lazy<HttpClient>>(
new ConcurrentQueue<Lazy<HttpClient>>(), 5);
77 for (
int i = 0; i < _clientPool.BoundedCapacity; i++)
79 _clientPool.Add(
new Lazy<HttpClient>());
86 public virtual void Initialize(
int userId,
string token,
string dataFolder)
89 _dataFolder = dataFolder?.Replace(
"\\",
"/", StringComparison.InvariantCulture);
92 JsonConvert.DefaultSettings = () =>
new JsonSerializerSettings
114 var request =
new RestRequest(
"projects/create", Method.POST)
116 RequestFormat = DataFormat.Json
121 if (
string.IsNullOrEmpty(organizationId))
123 jsonParams = JsonConvert.SerializeObject(
new
131 jsonParams = JsonConvert.SerializeObject(
new
139 request.AddParameter(
"application/json", jsonParams, ParameterType.RequestBody);
153 var request =
new RestRequest(
"projects/read", Method.POST)
155 RequestFormat = DataFormat.Json
158 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
161 }), ParameterType.RequestBody);
174 var request =
new RestRequest(
"projects/read", Method.POST)
176 RequestFormat = DataFormat.Json
194 var request =
new RestRequest(
"files/create", Method.POST)
196 RequestFormat = DataFormat.Json
199 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
204 }), ParameterType.RequestBody);
221 var request =
new RestRequest(
"files/update", Method.POST)
223 RequestFormat = DataFormat.Json
226 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
230 newName = newFileName
231 }), ParameterType.RequestBody);
248 var request =
new RestRequest(
"files/update", Method.POST)
250 RequestFormat = DataFormat.Json
253 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
257 content = newFileContents
258 }), ParameterType.RequestBody);
273 var request =
new RestRequest(
"files/read", Method.POST)
275 RequestFormat = DataFormat.Json
278 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
281 }), ParameterType.RequestBody);
294 var request =
new RestRequest(
"projects/nodes/read", Method.POST)
296 RequestFormat = DataFormat.Json
299 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
302 }), ParameterType.RequestBody);
317 var request =
new RestRequest(
"projects/nodes/update", Method.POST)
319 RequestFormat = DataFormat.Json
322 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
326 }), ParameterType.RequestBody);
341 var request =
new RestRequest(
"files/read", Method.POST)
343 RequestFormat = DataFormat.Json
346 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
350 }), ParameterType.RequestBody);
361 var request =
new RestRequest(
"lean/versions/read", Method.POST)
363 RequestFormat = DataFormat.Json
379 var request =
new RestRequest(
"files/delete", Method.POST)
381 RequestFormat = DataFormat.Json
384 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
388 }), ParameterType.RequestBody);
402 var request =
new RestRequest(
"projects/delete", Method.POST)
404 RequestFormat = DataFormat.Json
407 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
410 }), ParameterType.RequestBody);
424 var request =
new RestRequest(
"compile/create", Method.POST)
426 RequestFormat = DataFormat.Json
429 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
432 }), ParameterType.RequestBody);
447 var request =
new RestRequest(
"compile/read", Method.POST)
449 RequestFormat = DataFormat.Json
452 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
456 }), ParameterType.RequestBody);
470 throw new NotImplementedException($
"{nameof(Api)} does not support sending notifications");
483 var request =
new RestRequest(
"backtests/create", Method.POST)
485 RequestFormat = DataFormat.Json
488 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
493 }), ParameterType.RequestBody);
498 result.Backtest.Success = result.Success;
499 result.Backtest.Errors = result.Errors;
502 return result.Backtest;
515 var request =
new RestRequest(
"backtests/read", Method.POST)
517 RequestFormat = DataFormat.Json
520 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
524 }), ParameterType.RequestBody);
537 result.Backtest =
new Backtest { BacktestId = backtestId };
540 else if (getCharts && result.Backtest.Completed)
543 var updatedCharts =
new Dictionary<string, Chart>();
546 foreach (var chart
in result.Backtest.Charts)
548 if (!chart.Value.Series.IsNullOrEmpty())
553 var chartRequest =
new RestRequest(
"backtests/read", Method.POST)
555 RequestFormat = DataFormat.Json
558 chartRequest.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
563 }), ParameterType.RequestBody);
568 updatedCharts.Add(chart.Key, chartResponse.Backtest.Charts[chart.Key]);
573 foreach(var updatedChart
in updatedCharts)
575 result.Backtest.Charts[updatedChart.Key] = updatedChart.Value;
580 result.Backtest.Success = result.Success;
581 result.Backtest.Errors = result.Errors;
584 return result.Backtest;
597 public List<ApiOrderResponse>
ReadBacktestOrders(
int projectId,
string backtestId,
int start = 0,
int end = 100)
599 var request =
new RestRequest(
"backtests/orders/read", Method.POST)
601 RequestFormat = DataFormat.Json
604 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
610 }), ParameterType.RequestBody);
612 return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(
ReadBacktestOrders)).Orders;
627 var request =
new RestRequest(
"backtests/chart/read", Method.POST)
629 RequestFormat = DataFormat.Json
632 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
640 }), ParameterType.RequestBody);
645 var finish = DateTime.UtcNow.AddMinutes(1);
646 while (DateTime.UtcNow < finish && result.Chart ==
null)
666 var request =
new RestRequest(
"backtests/update", Method.POST)
668 RequestFormat = DataFormat.Json
671 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
677 }), ParameterType.RequestBody);
692 var request =
new RestRequest(
"backtests/list", Method.POST)
694 RequestFormat = DataFormat.Json
697 var obj =
new Dictionary<string, object>()
699 {
"projectId", projectId },
700 {
"includeStatistics", includeStatistics }
703 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
718 var request =
new RestRequest(
"backtests/delete", Method.POST)
720 RequestFormat = DataFormat.Json
723 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
727 }), ParameterType.RequestBody);
742 var request =
new RestRequest(
"backtests/tags/update", Method.POST)
744 RequestFormat = DataFormat.Json
747 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
752 }), ParameterType.RequestBody);
769 var request =
new RestRequest(
"backtests/insights/read", Method.POST)
771 RequestFormat = DataFormat.Json,
774 var diff = end - start;
777 throw new ArgumentException($
"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}.");
786 {
"projectId", projectId },
787 {
"backtestId", backtestId },
792 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
822 Dictionary<string, object> brokerageSettings,
823 string versionId =
"-1",
824 Dictionary<string, object> dataProviders =
null)
826 var request =
new RestRequest(
"live/create", Method.POST)
828 RequestFormat = DataFormat.Json
831 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
840 ), ParameterType.RequestBody);
870 return CreateLiveAlgorithm(projectId, compileId, nodeId, ConvertToDictionary(brokerageSettings), versionId, dataProviders !=
null ? ConvertToDictionary(dataProviders) :
null);
877 private static Dictionary<string, object> ConvertToDictionary(PyObject brokerageSettings)
881 var stringBrokerageSettings = brokerageSettings.ToString();
882 return JsonConvert.DeserializeObject<Dictionary<string, object>>(stringBrokerageSettings);
895 DateTime? startTime =
null,
896 DateTime? endTime =
null)
899 if (status.HasValue &&
905 throw new ArgumentException(
906 "The Api only supports Algorithm Statuses of Running, Stopped, RuntimeError and Liquidated");
909 var request =
new RestRequest(
"live/list", Method.POST)
911 RequestFormat = DataFormat.Json
917 JObject obj =
new JObject
919 {
"start", epochStartTime },
920 {
"end", epochEndTime }
925 obj.Add(
"status", status.ToString());
928 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
943 var request =
new RestRequest(
"live/read", Method.POST)
945 RequestFormat = DataFormat.Json
948 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
952 }), ParameterType.RequestBody);
965 var request =
new RestRequest(
"live/portfolio/read", Method.POST)
967 RequestFormat = DataFormat.Json
970 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
973 }), ParameterType.RequestBody);
988 public List<ApiOrderResponse>
ReadLiveOrders(
int projectId,
int start = 0,
int end = 100)
990 var request =
new RestRequest(
"live/orders/read", Method.POST)
992 RequestFormat = DataFormat.Json
995 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1000 }), ParameterType.RequestBody);
1002 return MakeRequestOrThrow<OrdersResponseWrapper>(request, nameof(
ReadLiveOrders)).Orders;
1013 var request =
new RestRequest(
"live/update/liquidate", Method.POST)
1015 RequestFormat = DataFormat.Json
1018 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1021 }), ParameterType.RequestBody);
1034 var request =
new RestRequest(
"live/update/stop", Method.POST)
1036 RequestFormat = DataFormat.Json
1039 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1042 }), ParameterType.RequestBody);
1056 var request =
new RestRequest(
"live/commands/create", Method.POST)
1058 RequestFormat = DataFormat.Json
1061 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1065 }), ParameterType.RequestBody);
1080 var request =
new RestRequest(
"live/commands/broadcast", Method.POST)
1082 RequestFormat = DataFormat.Json,
1085 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1090 }), ParameterType.RequestBody);
1106 var logLinesNumber = endLine - startLine;
1107 if (logLinesNumber > 250)
1109 throw new ArgumentException($
"The maximum number of log lines allowed is 250. But the number of log lines was {logLinesNumber}.");
1112 var request =
new RestRequest(
"live/logs/read", Method.POST)
1114 RequestFormat = DataFormat.Json
1117 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1124 }), ParameterType.RequestBody);
1141 var request =
new RestRequest(
"live/chart/read", Method.POST)
1143 RequestFormat = DataFormat.Json
1146 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1153 }), ParameterType.RequestBody);
1158 var finish = DateTime.UtcNow.AddMinutes(1);
1159 while(DateTime.UtcNow < finish && result.Chart ==
null)
1177 var request =
new RestRequest(
"live/insights/read", Method.POST)
1179 RequestFormat = DataFormat.Json,
1182 var diff = end - start;
1185 throw new ArgumentException($
"The difference between the start and end index of the insights must be smaller than 100, but it was {diff}.");
1192 JObject obj =
new JObject
1194 {
"projectId", projectId },
1199 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1213 if (filePath ==
null)
1215 throw new ArgumentException(
"Api.ReadDataLink(): Filepath must not be null");
1221 var request =
new RestRequest(
"data/read", Method.POST)
1223 RequestFormat = DataFormat.Json
1226 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1231 }), ParameterType.RequestBody);
1243 if (filePath ==
null)
1245 throw new ArgumentException(
"Api.ReadDataDirectory(): Filepath must not be null");
1253 if (filePath.Count(x => x ==
'/') < 3)
1255 throw new ArgumentException($
"Api.ReadDataDirectory(): Data directory requested must be at least" +
1256 $
" three directories deep. FilePath: {filePath}");
1259 var request =
new RestRequest(
"data/list", Method.POST)
1261 RequestFormat = DataFormat.Json
1264 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1267 }), ParameterType.RequestBody);
1278 var request =
new RestRequest(
"data/prices", Method.POST)
1280 RequestFormat = DataFormat.Json
1283 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1286 }), ParameterType.RequestBody);
1300 var request =
new RestRequest(
"backtests/read/report", Method.POST)
1302 RequestFormat = DataFormat.Json
1305 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1309 }), ParameterType.RequestBody);
1312 var finish = DateTime.UtcNow.AddMinutes(1);
1313 while (DateTime.UtcNow < finish && !report.
Success)
1315 Thread.Sleep(10000);
1334 if (!dataLink.Success)
1336 Log.
Trace($
"Api.DownloadData(): Failed to get link for {filePath}. " +
1337 $
"Errors: {string.Join(',', dataLink.Errors)}");
1342 var directory = Path.GetDirectoryName(filePath);
1343 if (!Directory.Exists(directory))
1345 Directory.CreateDirectory(directory);
1348 var client = BorrowClient();
1352 var uri =
new Uri(dataLink.Link);
1353 using var dataStream = client.Value.GetStreamAsync(uri);
1356 dataStream.Result.CopyTo(fileStream);
1360 Log.
Error($
"Api.DownloadData(): Failed to download zip for path ({filePath})");
1365 ReturnClient(client);
1381 ChartSubscription =
"*"
1412 public virtual void SendStatistics(
string algorithmId, decimal unrealized, decimal fees, decimal netProfit, decimal holdings, decimal equity, decimal netReturn, decimal volume,
int trades,
double sharpe)
1424 public virtual void SendUserEmail(
string algorithmId,
string subject,
string body)
1437 public virtual string Download(
string address, IEnumerable<KeyValuePair<string, string>> headers,
string userName,
string password)
1439 return Encoding.UTF8.GetString(
DownloadBytes(address, headers, userName, password));
1451 public virtual byte[]
DownloadBytes(
string address, IEnumerable<KeyValuePair<string, string>> headers,
string userName,
string password)
1453 var client = BorrowClient();
1456 client.Value.DefaultRequestHeaders.Clear();
1459 client.Value.DefaultRequestHeaders.TryAddWithoutValidation(
"user-agent",
"QCAlgorithm.Download(): User Agent Header");
1461 if (headers !=
null)
1463 foreach (var header
in headers)
1465 client.Value.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
1469 if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1471 var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($
"{userName}:{password}"));
1472 client.Value.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
"Basic", credentials);
1475 return client.Value.GetByteArrayAsync(
new Uri(address)).Result;
1477 catch (Exception exception)
1479 var message = $
"Api.DownloadBytes(): Failed to download data from {address}";
1480 if (!userName.IsNullOrEmpty() || !password.IsNullOrEmpty())
1482 message += $
" with username: {userName} and password {password}";
1485 throw new WebException($
"{message}. Please verify the source for missing http:// or https://", exception);
1489 client.Value.DefaultRequestHeaders.Clear();
1490 ReturnClient(client);
1501 _clientPool.CompleteAdding();
1502 foreach (var client
in _clientPool.GetConsumingEnumerable())
1504 if (client.IsValueCreated)
1506 client.Value.DisposeSafely();
1509 _clientPool.DisposeSafely();
1520 var data = $
"{token}:{timestamp.ToStringInvariant()}";
1521 return data.ToSHA256();
1530 var request =
new RestRequest(
"account/read", Method.POST)
1532 RequestFormat = DataFormat.Json
1535 if (organizationId !=
null)
1537 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new { organizationId }), ParameterType.RequestBody);
1551 var request =
new RestRequest(
"organizations/read", Method.POST)
1553 RequestFormat = DataFormat.Json
1556 if (organizationId !=
null)
1558 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new { organizationId }), ParameterType.RequestBody);
1562 return response.Organization;
1583 decimal? targetValue,
1586 HashSet<OptimizationParameter> parameters,
1587 IReadOnlyList<Constraint> constraints)
1589 var request =
new RestRequest(
"optimizations/estimate", Method.POST)
1591 RequestFormat = DataFormat.Json
1594 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1608 return response.Estimate;
1632 decimal? targetValue,
1635 HashSet<OptimizationParameter> parameters,
1636 IReadOnlyList<Constraint> constraints,
1637 decimal estimatedCost,
1641 var request =
new RestRequest(
"optimizations/create", Method.POST)
1643 RequestFormat = DataFormat.Json
1646 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1663 return result.Optimizations.FirstOrDefault();
1673 var request =
new RestRequest(
"optimizations/list", Method.POST)
1675 RequestFormat = DataFormat.Json
1678 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1681 }), ParameterType.RequestBody);
1684 return result.Optimizations;
1694 var request =
new RestRequest(
"optimizations/read", Method.POST)
1696 RequestFormat = DataFormat.Json
1699 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1702 }), ParameterType.RequestBody);
1705 return response.Optimization;
1715 var request =
new RestRequest(
"optimizations/abort", Method.POST)
1717 RequestFormat = DataFormat.Json
1720 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1723 }), ParameterType.RequestBody);
1737 var request =
new RestRequest(
"optimizations/update", Method.POST)
1739 RequestFormat = DataFormat.Json
1742 var obj =
new JObject
1744 {
"optimizationId", optimizationId }
1747 if (name.HasValue())
1749 obj.Add(
"name", name);
1752 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1765 var request =
new RestRequest(
"optimizations/delete", Method.POST)
1767 RequestFormat = DataFormat.Json
1770 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1773 }), ParameterType.RequestBody);
1786 public bool GetObjectStore(
string organizationId, List<string> keys,
string destinationFolder =
null)
1788 var request =
new RestRequest(
"object/get", Method.POST)
1790 RequestFormat = DataFormat.Json
1793 request.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1797 }), ParameterType.RequestBody);
1801 if (result ==
null || !result.Success)
1803 Log.
Error($
"Api.GetObjectStore(): Failed to get the jobId to request the download URL for the object store files."
1804 + (result !=
null ? $
" Errors: {string.Join(",
", result.Errors)}" :
""));
1808 var jobId = result.JobId;
1809 var getUrlRequest =
new RestRequest(
"object/get", Method.POST)
1811 RequestFormat = DataFormat.Json
1813 getUrlRequest.AddParameter(
"application/json", JsonConvert.SerializeObject(
new
1817 }), ParameterType.RequestBody);
1819 var frontier = DateTime.UtcNow + TimeSpan.FromMinutes(5);
1820 while (
string.IsNullOrEmpty(result?.Url) && (DateTime.UtcNow < frontier))
1826 if (result ==
null ||
string.IsNullOrEmpty(result.Url))
1828 Log.
Error($
"Api.GetObjectStore(): Failed to get the download URL from the jobId {jobId}."
1829 + (result !=
null ? $
" Errors: {string.Join(",
", result.Errors)}" :
""));
1833 var directory = destinationFolder ?? Directory.GetCurrentDirectory();
1834 var client = BorrowClient();
1838 if (client.Value.Timeout != TimeSpan.FromMinutes(20))
1840 client.Value.Timeout = TimeSpan.FromMinutes(20);
1844 var uri =
new Uri(result.Url);
1845 using var byteArray = client.Value.GetByteArrayAsync(uri);
1851 Log.
Error($
"Api.GetObjectStore(): Failed to download zip for path ({directory}). Error: {e.Message}");
1856 ReturnClient(client);
1871 var request =
new RestRequest(
"object/properties", Method.POST)
1873 RequestFormat = DataFormat.Json
1876 request.AddParameter(
"organizationId", organizationId);
1877 request.AddParameter(
"key", key);
1881 if (result ==
null || !result.Success)
1883 Log.
Error($
"Api.ObjectStore(): Failed to get the properties for the object store key {key}." + (result !=
null ? $
" Errors: {string.Join(",
", result.Errors)}" :
""));
1897 var request =
new RestRequest(
"object/set", Method.POST)
1899 RequestFormat = DataFormat.Json
1902 request.AddParameter(
"organizationId", organizationId);
1903 request.AddParameter(
"key", key);
1904 request.AddFileBytes(
"objectData", objectData,
"objectData");
1905 request.AlwaysMultipartFormData =
true;
1919 var request =
new RestRequest(
"object/delete", Method.POST)
1921 RequestFormat = DataFormat.Json
1924 var obj =
new Dictionary<string, object>
1926 {
"organizationId", organizationId },
1930 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1944 var request =
new RestRequest(
"object/list", Method.POST)
1946 RequestFormat = DataFormat.Json
1949 var obj =
new Dictionary<string, object>
1951 {
"organizationId", organizationId },
1955 request.AddParameter(
"application/json", JsonConvert.SerializeObject(obj), ParameterType.RequestBody);
1969 if (filePath ==
null)
1971 Log.
Error(
"Api.FormatPathForDataRequest(): Cannot format null string");
1977 dataFolder = dataFolder.Replace(
"\\",
"/", StringComparison.InvariantCulture);
1978 filePath = filePath.Replace(
"\\",
"/", StringComparison.InvariantCulture);
1981 if (filePath.StartsWith(dataFolder, StringComparison.InvariantCulture))
1983 filePath = filePath.Substring(dataFolder.Length);
1987 filePath = filePath.TrimStart(
'/');
1994 private T MakeRequestOrThrow<T>(RestRequest request,
string callerName)
1999 var errors =
string.Empty;
2000 if (result !=
null && result.Errors !=
null && result.Errors.Count > 0)
2002 errors = $
". Errors: ['{string.Join(",
", result.Errors)}']";
2004 throw new WebException($
"{callerName} api request failed{errors}");
2013 private Lazy<HttpClient> BorrowClient()
2015 using var cancellationTokenSource =
new CancellationTokenSource(TimeSpan.FromMinutes(10));
2016 return _clientPool.Take(cancellationTokenSource.Token);
2022 private void ReturnClient(Lazy<HttpClient> client)
2024 _clientPool.Add(client);