-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathKcHttpClientBase.cs
More file actions
334 lines (303 loc) · 14.9 KB
/
KcHttpClientBase.cs
File metadata and controls
334 lines (303 loc) · 14.9 KB
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using NETCore.Keycloak.Client.Exceptions;
using NETCore.Keycloak.Client.Models;
using NETCore.Keycloak.Client.Models.Common;
using NETCore.Keycloak.Client.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NETCore.Keycloak.Client.HttpClients;
/// <summary>
/// Base class for handling Keycloak HTTP client operations.
/// </summary>
public abstract class KcHttpClientBase
{
/// <summary>
/// Logger instance for logging operations.
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// The base URL for Keycloak API.
/// </summary>
protected string BaseUrl { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="KcHttpClientBase"/> class.
/// </summary>
/// <param name="logger">Logger instance to log information and errors.</param>
/// <param name="baseUrl">Keycloak base URL. Must not be null or empty.</param>
/// <exception cref="KcException">Thrown if the <paramref name="baseUrl"/> is null or empty.</exception>
protected KcHttpClientBase(ILogger logger, string baseUrl)
{
if ( string.IsNullOrWhiteSpace(baseUrl) )
{
throw new KcException($"{nameof(baseUrl)} is required");
}
// Ensure the base URL does not end with a trailing slash.
BaseUrl = baseUrl.EndsWith("/", StringComparison.Ordinal)
? baseUrl.Remove(baseUrl.Length - 1, 1)
: baseUrl;
Logger = logger;
}
/// <summary>
/// Validates the realm name and access token for a Keycloak operation.
/// </summary>
/// <param name="realm">The Keycloak realm name.</param>
/// <param name="accessToken">The access token for the realm administrator.</param>
/// <exception cref="KcException">Thrown if either <paramref name="realm"/> or <paramref name="accessToken"/> is null or empty.</exception>
protected static void ValidateAccess(string realm, string accessToken)
{
// Validate that the realm is not null or empty.
ValidateRequiredString(nameof(realm), realm);
// Validate that the access token is not null or empty.
ValidateRequiredString(nameof(accessToken), accessToken);
}
/// <summary>
/// Handles the response from a Keycloak HTTP request.
/// </summary>
/// <typeparam name="T">The type of the response data.</typeparam>
/// <param name="result">The HTTP response message.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="KcResponse{T}"/> object containing the response data or error message.</returns>
protected static async Task<KcResponse<T>> HandleAsync<T>(
KcHttpRequestExecutionResult result, CancellationToken cancellationToken = default) =>
result?.ResponseMessage?.IsSuccessStatusCode != true
? new KcResponse<T>
{
IsError = true,
ErrorMessage = result?.ResponseMessage != null
? await result.ResponseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)
: result?.Exception?.Message,
Exception = result?.Exception,
MonitoringMetrics = await KcHttpApiMonitoringMetrics
.MapFromHttpRequestExecutionResult(result, cancellationToken)
.ConfigureAwait(false)
}
: result.ResponseMessage.StatusCode == HttpStatusCode.NoContent
? new KcResponse<T>
{
MonitoringMetrics = await KcHttpApiMonitoringMetrics
.MapFromHttpRequestExecutionResult(result, cancellationToken)
.ConfigureAwait(false),
IsError = false
}
: new KcResponse<T>
{
Response = JsonConvert.DeserializeObject<T>(
await result.ResponseMessage.Content.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false)),
MonitoringMetrics = await KcHttpApiMonitoringMetrics
.MapFromHttpRequestExecutionResult(result, cancellationToken)
.ConfigureAwait(false)
};
/// <summary>
/// Executes an HTTP request and captures execution metrics, including fallback data in case of exceptions.
/// </summary>
/// <param name="request">The function representing the HTTP request to be executed.</param>
/// <param name="monitoringFallbackModel">
/// The monitoring fallback model used to track execution metrics and errors.
/// <see cref="KcHttpMonitoringFallbackModel"/>
/// </param>
/// <returns>
/// A task that represents the asynchronous operation, containing a <see cref="KcHttpMonitoringFallbackModel"/>
/// with the result of the HTTP request and its associated metrics.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="request"/> or <paramref name="monitoringFallbackModel"/> is null.
/// </exception>
protected static async Task<KcHttpRequestExecutionResult> ExecuteRequest(
Func<Task<HttpResponseMessage>> request,
KcHttpMonitoringFallbackModel monitoringFallbackModel)
{
// Ensure the request function is not null
ArgumentNullException.ThrowIfNull(request);
// Ensure the monitoring fallback model is not null
ArgumentNullException.ThrowIfNull(monitoringFallbackModel);
// Start a timer to measure request execution time
var timer = Stopwatch.StartNew();
try
{
// Execute the HTTP request
var response = await request.Invoke().ConfigureAwait(false);
// Stop the timer after the request completes successfully
timer.Stop();
// Record the elapsed time in the monitoring fallback model
monitoringFallbackModel.RequestMilliseconds = timer.ElapsedMilliseconds;
// Return the execution result with the captured metrics and response data
return new KcHttpRequestExecutionResult
{
RequestMilliseconds = timer.ElapsedMilliseconds,
ResponseMessage = response,
RequestMessage = response.RequestMessage,
MonitoringFallback = monitoringFallbackModel
};
}
catch ( Exception e )
{
// Stop the timer if an exception occurs
timer.Stop();
// Record the elapsed time in the monitoring fallback model
monitoringFallbackModel.RequestMilliseconds = timer.ElapsedMilliseconds;
// Return the execution result with the exception details and fallback metrics
return new KcHttpRequestExecutionResult
{
RequestMilliseconds = timer.ElapsedMilliseconds,
Exception = e,
MonitoringFallback = monitoringFallbackModel
};
}
}
/// <summary>
/// Processes an HTTP request and returns a response of the specified type.
/// </summary>
/// <typeparam name="T">The type of the expected response data.</typeparam>
/// <param name="url">The URL for the HTTP request.</param>
/// <param name="method">The HTTP method (e.g., GET, POST, PUT, DELETE) to use for the request.</param>
/// <param name="accessToken">Optional: The access token to include in the request headers for authentication.</param>
/// <param name="errorMessage">The error message to log if the request fails.</param>
/// <param name="content">Optional: The content to include in the request body, if applicable.</param>
/// <param name="contentType">The content type header for the request. Defaults to "application/json".</param>
/// <param name="cancellationToken">Optional: A token to monitor for cancellation requests.</param>
/// <returns>
/// A <see cref="KcResponse{T}"/> object containing the response data or error details.
/// </returns>
/// <remarks>
/// This method handles the execution of an HTTP request, including logging errors and managing exceptions.
/// It creates the request, sends it using an <see cref="HttpClient"/>, and processes the response.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="url"/> or <paramref name="method"/> is null or empty.</exception>
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the <paramref name="cancellationToken"/>.</exception>
protected async Task<KcResponse<T>> ProcessRequestAsync<T>(
string url,
HttpMethod method,
string accessToken,
string errorMessage,
object content = null,
string contentType = "application/json",
CancellationToken cancellationToken = default)
{
try
{
// Execute the HTTP request and process the response.
using var response = await ExecuteRequest(async () =>
{
// Create the HTTP request based on the specified method and content.
using var request = content == null
? CreateRequest(method, url, accessToken,
contentType: contentType) // For requests without a body (e.g., GET).
: CreateRequest(method, url, accessToken,
GetBody(content), contentType: contentType); // For requests with a body (e.g., POST, PUT).
// Create a new HttpClient instance for sending the request.
using var client = new HttpClient();
// Send the request asynchronously and return the response.
return await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
}, new KcHttpMonitoringFallbackModel
{
Url = url, // Log the URL for monitoring purposes.
HttpMethod = method // Log the HTTP method for monitoring purposes.
}).ConfigureAwait(false);
// Handle the response and deserialize it to the specified type.
return await HandleAsync<T>(response, cancellationToken).ConfigureAwait(false);
}
catch ( Exception e )
{
// Log the error details if a logger instance is available.
if ( Logger != null )
{
KcLoggerMessages.Error(Logger, errorMessage, e);
}
// Return a response object indicating an error occurred.
return new KcResponse<T>
{
IsError = true, // Mark the response as an error.
Exception = e, // Attach the exception for debugging purposes.
ErrorMessage = e.Message,
MonitoringMetrics = new KcHttpApiMonitoringMetrics
{
HttpMethod = method,
Url = new Uri(url),
Error = e.Message,
RequestException = e
}
};
}
}
/// <summary>
/// Validates that a required string parameter is not null, empty, or whitespace.
/// </summary>
/// <param name="paramName">The name of the parameter being validated, used in the exception message.</param>
/// <param name="value">The value of the string parameter to validate.</param>
/// <exception cref="KcException">Thrown when the string parameter is null, empty, or contains only whitespace.</exception>
protected static void ValidateRequiredString(string paramName, string value)
{
if ( string.IsNullOrWhiteSpace(value) )
{
throw new KcException($"{paramName} is required");
}
}
/// <summary>
/// Validates that a required object parameter is not null.
/// </summary>
/// <param name="paramName">The name of the parameter being validated, used in the exception message.</param>
/// <param name="value">The object parameter to validate.</param>
/// <exception cref="KcException">Thrown when the object parameter is null.</exception>
protected static void ValidateNotNull(string paramName, object value)
{
if ( value == null )
{
throw new KcException($"{paramName} is required");
}
}
/// <summary>
/// Creates an HTTP request message for Keycloak operations.
/// </summary>
/// <param name="method">The HTTP method (e.g., GET, POST).</param>
/// <param name="endpoint">The endpoint URL.</param>
/// <param name="accessToken">The access token for authorization.</param>
/// <param name="content">Optional request content.</param>
/// <param name="contentType">The content type header for the request. Defaults to "application/json".</param>
/// <returns>An <see cref="HttpRequestMessage"/> configured with the specified parameters.</returns>
private static HttpRequestMessage CreateRequest(
HttpMethod method,
string endpoint,
string accessToken,
HttpContent content = null,
string contentType = "application/json")
{
// Initialize the HTTP request message with the provided method, endpoint, and content.
var request = new HttpRequestMessage
{
Method = method,
RequestUri = new Uri(endpoint),
Content = content ?? new StringContent(string.Empty) // Use empty content if none is provided.
};
// Configure the content type header if the content exists.
if ( request.Content.Headers.ContentType != null )
{
request.Content.Headers.ContentType.MediaType = contentType; // Set the media type for the content.
request.Content.Headers.ContentType.CharSet = null; // Clear the charset to ensure compatibility.
}
// Add the Accept header to indicate the expected response content type.
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(contentType));
// Add the Authorization header with the access token if it is not null or empty.
if ( !string.IsNullOrWhiteSpace(accessToken) )
{
_ = request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {accessToken}");
}
// Return the configured HTTP request message.
return request;
}
/// <summary>
/// Builds the body for an HTTP request.
/// </summary>
/// <param name="o">The object to serialize as the request body.</param>
/// <returns>A <see cref="StringContent"/> containing the serialized object.</returns>
private static StringContent GetBody(object o) =>
new(JsonConvert.SerializeObject(o, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Converters = [new StringEnumConverter()]
}));
}