mdan-cli 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +28 -0
- package/README.md +152 -5
- package/agents/auto-orchestrator.md +343 -0
- package/agents/devops.md +511 -94
- package/cli/mdan.js +1 -1
- package/cli/mdan.py +75 -4
- package/cli/mdan.sh +1 -1
- package/core/debate-protocol.md +454 -0
- package/core/universal-envelope.md +113 -0
- package/memory/CONTEXT-SAVE-FORMAT.md +328 -0
- package/memory/MEMORY-AUTO.json +66 -0
- package/memory/RESUME-PROTOCOL.md +379 -0
- package/package.json +1 -1
- package/phases/auto-01-load.md +165 -0
- package/phases/auto-02-discover.md +207 -0
- package/phases/auto-03-plan.md +509 -0
- package/phases/auto-04-architect.md +567 -0
- package/phases/auto-05-implement.md +713 -0
- package/phases/auto-06-test.md +559 -0
- package/phases/auto-07-deploy.md +510 -0
- package/phases/auto-08-doc.md +970 -0
- package/skills/azure-devops/skill.md +1757 -0
- package/templates/dotnet-blazor/README.md +415 -0
- package/templates/external-services/ExampleService.cs +361 -0
- package/templates/external-services/IService.cs +113 -0
- package/templates/external-services/README.md +325 -0
- package/templates/external-services/ServiceBase.cs +492 -0
- package/templates/external-services/ServiceProvider.cs +243 -0
- package/templates/prompts/devops-agent.yaml +327 -0
- package/templates/prompts.json +15 -1
- package/templates/sql-server/README.md +37 -0
- package/templates/sql-server/functions.sql +158 -0
- package/templates/sql-server/schema.sql +188 -0
- package/templates/sql-server/stored-procedures.sql +284 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Net;
|
|
5
|
+
using System.Net.Http;
|
|
6
|
+
using System.Net.Http.Headers;
|
|
7
|
+
using System.Text;
|
|
8
|
+
using System.Text.Json;
|
|
9
|
+
using System.Threading;
|
|
10
|
+
using System.Threading.Tasks;
|
|
11
|
+
using Microsoft.Extensions.Caching.Memory;
|
|
12
|
+
using Microsoft.Extensions.Configuration;
|
|
13
|
+
using Microsoft.Extensions.Logging;
|
|
14
|
+
using Polly;
|
|
15
|
+
using Polly.CircuitBreaker;
|
|
16
|
+
using Polly.RateLimit;
|
|
17
|
+
|
|
18
|
+
namespace ExternalServices.Services
|
|
19
|
+
{
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// Implémentation de base pour les services externes avec patterns avancés.
|
|
22
|
+
/// </summary>
|
|
23
|
+
public abstract class ServiceBase : IService
|
|
24
|
+
{
|
|
25
|
+
protected readonly HttpClient _httpClient;
|
|
26
|
+
protected readonly IConfiguration _configuration;
|
|
27
|
+
protected readonly ILogger _logger;
|
|
28
|
+
protected readonly IMemoryCache _cache;
|
|
29
|
+
protected readonly string _serviceName;
|
|
30
|
+
protected readonly string _baseUrl;
|
|
31
|
+
protected readonly string _apiKey;
|
|
32
|
+
protected readonly int _timeout;
|
|
33
|
+
protected readonly int _retryCount;
|
|
34
|
+
protected readonly int _retryDelay;
|
|
35
|
+
protected readonly bool _enableCircuitBreaker;
|
|
36
|
+
protected readonly int _circuitBreakerThreshold;
|
|
37
|
+
protected readonly bool _enableRateLimiting;
|
|
38
|
+
protected readonly int _rateLimitPerMinute;
|
|
39
|
+
protected readonly bool _enableCaching;
|
|
40
|
+
protected readonly int _cacheDurationMinutes;
|
|
41
|
+
|
|
42
|
+
protected string? _accessToken;
|
|
43
|
+
protected DateTime _tokenExpiry;
|
|
44
|
+
protected readonly SemaphoreSlim _rateLimitSemaphore;
|
|
45
|
+
protected readonly Queue<DateTime> _requestTimestamps;
|
|
46
|
+
protected AsyncCircuitBreakerPolicy? _circuitBreakerPolicy;
|
|
47
|
+
protected AsyncRetryPolicy? _retryPolicy;
|
|
48
|
+
|
|
49
|
+
protected ServiceStatus _status;
|
|
50
|
+
|
|
51
|
+
public string ServiceName => _serviceName;
|
|
52
|
+
public string BaseUrl => _baseUrl;
|
|
53
|
+
|
|
54
|
+
protected ServiceBase(
|
|
55
|
+
IConfiguration configuration,
|
|
56
|
+
string serviceName,
|
|
57
|
+
ILogger logger,
|
|
58
|
+
IMemoryCache? cache = null)
|
|
59
|
+
{
|
|
60
|
+
_configuration = configuration;
|
|
61
|
+
_serviceName = serviceName;
|
|
62
|
+
_logger = logger;
|
|
63
|
+
_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
|
|
64
|
+
_rateLimitSemaphore = new SemaphoreSlim(1, 1);
|
|
65
|
+
_requestTimestamps = new Queue<DateTime>();
|
|
66
|
+
_status = new ServiceStatus();
|
|
67
|
+
|
|
68
|
+
var configSection = configuration.GetSection($"ExternalServices:{serviceName}");
|
|
69
|
+
|
|
70
|
+
_baseUrl = configSection["BaseUrl"] ?? throw new ArgumentException($"BaseUrl not configured for service {serviceName}");
|
|
71
|
+
_apiKey = configSection["ApiKey"] ?? string.Empty;
|
|
72
|
+
_timeout = configSection.GetValue<int>("Timeout", 30);
|
|
73
|
+
_retryCount = configSection.GetValue<int>("RetryCount", 3);
|
|
74
|
+
_retryDelay = configSection.GetValue<int>("RetryDelay", 1000);
|
|
75
|
+
_enableCircuitBreaker = configSection.GetValue<bool>("EnableCircuitBreaker", true);
|
|
76
|
+
_circuitBreakerThreshold = configSection.GetValue<int>("CircuitBreakerThreshold", 5);
|
|
77
|
+
_enableRateLimiting = configSection.GetValue<bool>("EnableRateLimiting", true);
|
|
78
|
+
_rateLimitPerMinute = configSection.GetValue<int>("RateLimitPerMinute", 60);
|
|
79
|
+
_enableCaching = configSection.GetValue<bool>("EnableCaching", true);
|
|
80
|
+
_cacheDurationMinutes = configSection.GetValue<int>("CacheDurationMinutes", 5);
|
|
81
|
+
|
|
82
|
+
_httpClient = new HttpClient
|
|
83
|
+
{
|
|
84
|
+
BaseAddress = new Uri(_baseUrl),
|
|
85
|
+
Timeout = TimeSpan.FromSeconds(_timeout)
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
_httpClient.DefaultRequestHeaders.Accept.Add(
|
|
89
|
+
new MediaTypeWithQualityHeaderValue("application/json"));
|
|
90
|
+
|
|
91
|
+
InitializePolicies();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private void InitializePolicies()
|
|
95
|
+
{
|
|
96
|
+
// Retry Policy
|
|
97
|
+
_retryPolicy = Policy
|
|
98
|
+
.Handle<HttpRequestException>()
|
|
99
|
+
.OrResult<HttpResponseMessage>(r =>
|
|
100
|
+
r.StatusCode == HttpStatusCode.ServiceUnavailable ||
|
|
101
|
+
r.StatusCode == HttpStatusCode.GatewayTimeout ||
|
|
102
|
+
r.StatusCode == HttpStatusCode.RequestTimeout)
|
|
103
|
+
.WaitAndRetryAsync(
|
|
104
|
+
_retryCount,
|
|
105
|
+
retryAttempt => TimeSpan.FromMilliseconds(_retryDelay * retryAttempt),
|
|
106
|
+
onRetry: (outcome, timespan, retryCount, context) =>
|
|
107
|
+
{
|
|
108
|
+
_logger.LogWarning(
|
|
109
|
+
"Retry {RetryCount} after {Delay}ms for {ServiceName}",
|
|
110
|
+
retryCount, timespan.TotalMilliseconds, _serviceName);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Circuit Breaker Policy
|
|
114
|
+
if (_enableCircuitBreaker)
|
|
115
|
+
{
|
|
116
|
+
_circuitBreakerPolicy = Policy
|
|
117
|
+
.Handle<HttpRequestException>()
|
|
118
|
+
.OrResult<HttpResponseMessage>(r =>
|
|
119
|
+
r.StatusCode == HttpStatusCode.ServiceUnavailable ||
|
|
120
|
+
r.StatusCode == HttpStatusCode.GatewayTimeout)
|
|
121
|
+
.CircuitBreakerAsync(
|
|
122
|
+
exceptionsAllowedBeforeBreaking: _circuitBreakerThreshold,
|
|
123
|
+
durationOfBreak: TimeSpan.FromMinutes(1),
|
|
124
|
+
onBreak: (exception, breakDelay) =>
|
|
125
|
+
{
|
|
126
|
+
_status.IsCircuitOpen = true;
|
|
127
|
+
_logger.LogError(
|
|
128
|
+
"Circuit broken for {ServiceName} for {BreakDelay}ms",
|
|
129
|
+
_serviceName, breakDelay.TotalMilliseconds);
|
|
130
|
+
},
|
|
131
|
+
onReset: () =>
|
|
132
|
+
{
|
|
133
|
+
_status.IsCircuitOpen = false;
|
|
134
|
+
_status.ConsecutiveFailures = 0;
|
|
135
|
+
_logger.LogInformation("Circuit reset for {ServiceName}", _serviceName);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public virtual async Task AuthenticateAsync()
|
|
141
|
+
{
|
|
142
|
+
if (_accessToken != null && DateTime.UtcNow < _tokenExpiry)
|
|
143
|
+
{
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try
|
|
148
|
+
{
|
|
149
|
+
var authRequest = new
|
|
150
|
+
{
|
|
151
|
+
ApiKey = _apiKey
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
var response = await ExecuteWithRetryAsync(async () =>
|
|
155
|
+
{
|
|
156
|
+
var json = JsonSerializer.Serialize(authRequest);
|
|
157
|
+
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
158
|
+
return await _httpClient.PostAsync("auth/token", content);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (response.IsSuccessStatusCode)
|
|
162
|
+
{
|
|
163
|
+
var responseContent = await response.Content.ReadAsStringAsync();
|
|
164
|
+
var authResponse = JsonSerializer.Deserialize<AuthResponse>(responseContent);
|
|
165
|
+
|
|
166
|
+
if (authResponse != null)
|
|
167
|
+
{
|
|
168
|
+
_accessToken = authResponse.AccessToken;
|
|
169
|
+
_tokenExpiry = DateTime.UtcNow.AddSeconds(authResponse.ExpiresIn);
|
|
170
|
+
_status.IsAuthenticated = true;
|
|
171
|
+
_logger.LogInformation("Authenticated successfully for {ServiceName}", _serviceName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
_logger.LogError("Authentication failed for {ServiceName}: {StatusCode}",
|
|
177
|
+
_serviceName, response.StatusCode);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (Exception ex)
|
|
181
|
+
{
|
|
182
|
+
_logger.LogError(ex, "Authentication error for {ServiceName}", _serviceName);
|
|
183
|
+
throw;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async Task<bool> IsAuthenticatedAsync()
|
|
188
|
+
{
|
|
189
|
+
return _accessToken != null && DateTime.UtcNow < _tokenExpiry;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public async Task<ServiceResponse<T>> GetDataAsync<T>(string endpoint, Dictionary<string, string>? parameters = null)
|
|
193
|
+
{
|
|
194
|
+
return await ExecuteWithCircuitBreakerAsync(async () =>
|
|
195
|
+
{
|
|
196
|
+
await EnsureAuthenticatedAsync();
|
|
197
|
+
await ApplyRateLimitAsync();
|
|
198
|
+
|
|
199
|
+
var cacheKey = $"GET:{endpoint}:{GetCacheKey(parameters)}";
|
|
200
|
+
|
|
201
|
+
if (_enableCaching)
|
|
202
|
+
{
|
|
203
|
+
var cached = await GetFromCacheAsync<T>(cacheKey);
|
|
204
|
+
if (cached != null)
|
|
205
|
+
{
|
|
206
|
+
return cached;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var url = BuildUrl(endpoint, parameters);
|
|
211
|
+
var response = await ExecuteWithRetryAsync(async () =>
|
|
212
|
+
{
|
|
213
|
+
_httpClient.DefaultRequestHeaders.Authorization =
|
|
214
|
+
new AuthenticationHeaderValue("Bearer", _accessToken);
|
|
215
|
+
return await _httpClient.GetAsync(url);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
var result = await ProcessResponseAsync<T>(response);
|
|
219
|
+
|
|
220
|
+
if (_enableCaching && result.Success)
|
|
221
|
+
{
|
|
222
|
+
await SetCacheAsync(cacheKey, result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public async Task<ServiceResponse<T>> PostDataAsync<T>(string endpoint, object data)
|
|
230
|
+
{
|
|
231
|
+
return await ExecuteWithCircuitBreakerAsync(async () =>
|
|
232
|
+
{
|
|
233
|
+
await EnsureAuthenticatedAsync();
|
|
234
|
+
await ApplyRateLimitAsync();
|
|
235
|
+
|
|
236
|
+
var json = JsonSerializer.Serialize(data);
|
|
237
|
+
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
238
|
+
|
|
239
|
+
var response = await ExecuteWithRetryAsync(async () =>
|
|
240
|
+
{
|
|
241
|
+
_httpClient.DefaultRequestHeaders.Authorization =
|
|
242
|
+
new AuthenticationHeaderValue("Bearer", _accessToken);
|
|
243
|
+
return await _httpClient.PostAsync(endpoint, content);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return await ProcessResponseAsync<T>(response);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public async Task<ServiceResponse<T>> PutDataAsync<T>(string endpoint, object data)
|
|
251
|
+
{
|
|
252
|
+
return await ExecuteWithCircuitBreakerAsync(async () =>
|
|
253
|
+
{
|
|
254
|
+
await EnsureAuthenticatedAsync();
|
|
255
|
+
await ApplyRateLimitAsync();
|
|
256
|
+
|
|
257
|
+
var json = JsonSerializer.Serialize(data);
|
|
258
|
+
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
259
|
+
|
|
260
|
+
var response = await ExecuteWithRetryAsync(async () =>
|
|
261
|
+
{
|
|
262
|
+
_httpClient.DefaultRequestHeaders.Authorization =
|
|
263
|
+
new AuthenticationHeaderValue("Bearer", _accessToken);
|
|
264
|
+
return await _httpClient.PutAsync(endpoint, content);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return await ProcessResponseAsync<T>(response);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public async Task<ServiceResponse<T>> DeleteDataAsync<T>(string endpoint)
|
|
272
|
+
{
|
|
273
|
+
return await ExecuteWithCircuitBreakerAsync(async () =>
|
|
274
|
+
{
|
|
275
|
+
await EnsureAuthenticatedAsync();
|
|
276
|
+
await ApplyRateLimitAsync();
|
|
277
|
+
|
|
278
|
+
var response = await ExecuteWithRetryAsync(async () =>
|
|
279
|
+
{
|
|
280
|
+
_httpClient.DefaultRequestHeaders.Authorization =
|
|
281
|
+
new AuthenticationHeaderValue("Bearer", _accessToken);
|
|
282
|
+
return await _httpClient.DeleteAsync(endpoint);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return await ProcessResponseAsync<T>(response);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public async Task<bool> HealthCheckAsync()
|
|
290
|
+
{
|
|
291
|
+
try
|
|
292
|
+
{
|
|
293
|
+
var response = await _httpClient.GetAsync("health");
|
|
294
|
+
var isHealthy = response.IsSuccessStatusCode;
|
|
295
|
+
|
|
296
|
+
_status.IsHealthy = isHealthy;
|
|
297
|
+
|
|
298
|
+
if (isHealthy)
|
|
299
|
+
{
|
|
300
|
+
_status.LastSuccessfulCall = DateTime.UtcNow;
|
|
301
|
+
_status.ConsecutiveFailures = 0;
|
|
302
|
+
}
|
|
303
|
+
else
|
|
304
|
+
{
|
|
305
|
+
_status.LastFailedCall = DateTime.UtcNow;
|
|
306
|
+
_status.ConsecutiveFailures++;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return isHealthy;
|
|
310
|
+
}
|
|
311
|
+
catch (Exception ex)
|
|
312
|
+
{
|
|
313
|
+
_logger.LogError(ex, "Health check failed for {ServiceName}", _serviceName);
|
|
314
|
+
_status.IsHealthy = false;
|
|
315
|
+
_status.LastFailedCall = DateTime.UtcNow;
|
|
316
|
+
_status.ConsecutiveFailures++;
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public async Task<ServiceStatus> GetStatusAsync()
|
|
322
|
+
{
|
|
323
|
+
await HealthCheckAsync();
|
|
324
|
+
return _status;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
protected async Task EnsureAuthenticatedAsync()
|
|
328
|
+
{
|
|
329
|
+
if (!await IsAuthenticatedAsync())
|
|
330
|
+
{
|
|
331
|
+
await AuthenticateAsync();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
protected async Task ApplyRateLimitAsync()
|
|
336
|
+
{
|
|
337
|
+
if (!_enableRateLimiting)
|
|
338
|
+
{
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await _rateLimitSemaphore.WaitAsync();
|
|
343
|
+
try
|
|
344
|
+
{
|
|
345
|
+
var now = DateTime.UtcNow;
|
|
346
|
+
|
|
347
|
+
// Remove timestamps older than 1 minute
|
|
348
|
+
while (_requestTimestamps.Count > 0 &&
|
|
349
|
+
(now - _requestTimestamps.Peek()).TotalMinutes > 1)
|
|
350
|
+
{
|
|
351
|
+
_requestTimestamps.Dequeue();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check if rate limit exceeded
|
|
355
|
+
if (_requestTimestamps.Count >= _rateLimitPerMinute)
|
|
356
|
+
{
|
|
357
|
+
var oldestRequest = _requestTimestamps.Peek();
|
|
358
|
+
var waitTime = TimeSpan.FromMinutes(1) - (now - oldestRequest);
|
|
359
|
+
|
|
360
|
+
if (waitTime.TotalMilliseconds > 0)
|
|
361
|
+
{
|
|
362
|
+
_logger.LogWarning(
|
|
363
|
+
"Rate limit reached for {ServiceName}, waiting {WaitTime}ms",
|
|
364
|
+
_serviceName, waitTime.TotalMilliseconds);
|
|
365
|
+
await Task.Delay(waitTime);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_requestTimestamps.Enqueue(now);
|
|
370
|
+
}
|
|
371
|
+
finally
|
|
372
|
+
{
|
|
373
|
+
_rateLimitSemaphore.Release();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
protected async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> action)
|
|
378
|
+
{
|
|
379
|
+
if (_retryPolicy != null)
|
|
380
|
+
{
|
|
381
|
+
return await _retryPolicy.ExecuteAsync(action);
|
|
382
|
+
}
|
|
383
|
+
return await action();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
protected async Task<T> ExecuteWithCircuitBreakerAsync<T>(Func<Task<T>> action)
|
|
387
|
+
{
|
|
388
|
+
if (_circuitBreakerPolicy != null)
|
|
389
|
+
{
|
|
390
|
+
return await _circuitBreakerPolicy.ExecuteAsync(action);
|
|
391
|
+
}
|
|
392
|
+
return await action();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
protected async Task<ServiceResponse<T>> ProcessResponseAsync<T>(HttpResponseMessage response)
|
|
396
|
+
{
|
|
397
|
+
_status.TotalCalls++;
|
|
398
|
+
|
|
399
|
+
var result = new ServiceResponse<T>
|
|
400
|
+
{
|
|
401
|
+
Timestamp = DateTime.UtcNow,
|
|
402
|
+
StatusCode = (int)response.StatusCode
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
try
|
|
406
|
+
{
|
|
407
|
+
var responseContent = await response.Content.ReadAsStringAsync();
|
|
408
|
+
|
|
409
|
+
if (response.IsSuccessStatusCode)
|
|
410
|
+
{
|
|
411
|
+
result.Data = JsonSerializer.Deserialize<T>(responseContent);
|
|
412
|
+
result.Success = true;
|
|
413
|
+
_status.SuccessfulCalls++;
|
|
414
|
+
_status.LastSuccessfulCall = DateTime.UtcNow;
|
|
415
|
+
_status.ConsecutiveFailures = 0;
|
|
416
|
+
}
|
|
417
|
+
else
|
|
418
|
+
{
|
|
419
|
+
result.Success = false;
|
|
420
|
+
result.ErrorMessage = responseContent;
|
|
421
|
+
result.ErrorCode = ((int)response.StatusCode).ToString();
|
|
422
|
+
_status.FailedCalls++;
|
|
423
|
+
_status.LastFailedCall = DateTime.UtcNow;
|
|
424
|
+
_status.ConsecutiveFailures++;
|
|
425
|
+
|
|
426
|
+
_logger.LogWarning(
|
|
427
|
+
"Request failed for {ServiceName}: {StatusCode} - {ErrorMessage}",
|
|
428
|
+
_serviceName, response.StatusCode, responseContent);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (Exception ex)
|
|
432
|
+
{
|
|
433
|
+
result.Success = false;
|
|
434
|
+
result.ErrorMessage = ex.Message;
|
|
435
|
+
_status.FailedCalls++;
|
|
436
|
+
_status.LastFailedCall = DateTime.UtcNow;
|
|
437
|
+
_status.ConsecutiveFailures++;
|
|
438
|
+
|
|
439
|
+
_logger.LogError(ex, "Error processing response for {ServiceName}", _serviceName);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
protected string BuildUrl(string endpoint, Dictionary<string, string>? parameters)
|
|
446
|
+
{
|
|
447
|
+
if (parameters == null || parameters.Count == 0)
|
|
448
|
+
{
|
|
449
|
+
return endpoint;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
var queryString = string.Join("&", parameters.Select(kvp =>
|
|
453
|
+
$"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
|
|
454
|
+
return $"{endpoint}?{queryString}";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
protected string GetCacheKey(Dictionary<string, string>? parameters)
|
|
458
|
+
{
|
|
459
|
+
if (parameters == null || parameters.Count == 0)
|
|
460
|
+
{
|
|
461
|
+
return "default";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return string.Join("|", parameters.OrderBy(kvp => kvp.Key)
|
|
465
|
+
.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
protected async Task<ServiceResponse<T>?> GetFromCacheAsync<T>(string cacheKey)
|
|
469
|
+
{
|
|
470
|
+
if (_cache.TryGetValue(cacheKey, out ServiceResponse<T>? cached))
|
|
471
|
+
{
|
|
472
|
+
_logger.LogDebug("Cache hit for {CacheKey}", cacheKey);
|
|
473
|
+
return cached;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
protected async Task SetCacheAsync<T>(string cacheKey, ServiceResponse<T> value)
|
|
479
|
+
{
|
|
480
|
+
var expiry = TimeSpan.FromMinutes(_cacheDurationMinutes);
|
|
481
|
+
_cache.Set(cacheKey, value, expiry);
|
|
482
|
+
_logger.LogDebug("Cached response for {CacheKey} for {Expiry} minutes",
|
|
483
|
+
cacheKey, _cacheDurationMinutes);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
protected class AuthResponse
|
|
487
|
+
{
|
|
488
|
+
public string AccessToken { get; set; } = string.Empty;
|
|
489
|
+
public int ExpiresIn { get; set; }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|