-
Notifications
You must be signed in to change notification settings - Fork 33
/
Copy pathHttpRequestOfTContext.cs
375 lines (320 loc) · 11.7 KB
/
HttpRequestOfTContext.cs
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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
namespace KubeClient.Http
{
using Utilities;
using ValueProviders;
using RequestProperties = ImmutableDictionary<string, object>;
/// <summary>
/// A template for an HTTP request that resolves deferred values from an instance of <typeparamref name="TContext"/>.
/// </summary>
/// <typeparam name="TContext">
/// The type of object used as a context for resolving deferred values.
/// </typeparam>
public class HttpRequest<TContext>
: HttpRequestBase, IHttpRequest<TContext>
{
#region Constants
/// <summary>
/// The base properties for <see cref="HttpRequest"/>s.
/// </summary>
static readonly RequestProperties BaseProperties =
new Dictionary<string, object>
{
[nameof(RequestActions)] = ImmutableList<RequestAction<TContext>>.Empty,
[nameof(ResponseActions)] = ImmutableList<ResponseAction<TContext>>.Empty,
[nameof(TemplateParameters)] = ImmutableDictionary<string, IValueProvider<TContext, string>>.Empty,
[nameof(QueryParameters)] = ImmutableDictionary<string, IValueProvider<TContext, string>>.Empty
}
.ToImmutableDictionary();
/// <summary>
/// An empty <see cref="HttpRequest{TContext}"/>.
/// </summary>
public static HttpRequest<TContext> Empty = new HttpRequest<TContext>(BaseProperties);
/// <summary>
/// The default factory for <see cref="HttpRequest{TContext}"/>s.
/// </summary>
public static HttpRequestFactory<TContext> Factory { get; } = new HttpRequestFactory<TContext>(Empty);
#endregion // Constants
#region Construction
/// <summary>
/// Create a new HTTP request.
/// </summary>
/// <param name="properties">
/// The request properties.
/// </param>
HttpRequest(ImmutableDictionary<string, object> properties)
: base(properties)
{
EnsurePropertyType<ImmutableList<RequestAction<TContext>>>(
propertyName: nameof(RequestActions)
);
EnsurePropertyType<ImmutableDictionary<string, IValueProvider<TContext, string>>>(
propertyName: nameof(TemplateParameters)
);
EnsurePropertyType<ImmutableDictionary<string, IValueProvider<TContext, string>>>(
propertyName: nameof(QueryParameters)
);
}
/// <summary>
/// Create a new HTTP request with the specified request URI.
/// </summary>
/// <param name="requestUri">
/// The request URI (can be relative or absolute).
/// </param>
/// <returns>
/// The new <see cref="HttpRequest{TContext}"/>.
/// </returns>
public static HttpRequest<TContext> Create(string requestUri) => Factory.Create(requestUri);
/// <summary>
/// Create a new HTTP request with the specified request URI.
/// </summary>
/// <param name="requestUri">
/// The request URI (can be relative or absolute).
/// </param>
/// <returns>
/// The new <see cref="HttpRequest{TContext}"/>.
/// </returns>
public static HttpRequest<TContext> Create(Uri requestUri) => Factory.Create(requestUri);
#endregion // Construction
#region Properties
/// <summary>
/// Actions (if any) to perform on the outgoing request message.
/// </summary>
public ImmutableList<RequestAction<TContext>> RequestActions => GetProperty<ImmutableList<RequestAction<TContext>>>();
/// <summary>
/// Actions (if any) to perform on the incoming response message.
/// </summary>
public ImmutableList<ResponseAction<TContext>> ResponseActions => GetProperty<ImmutableList<ResponseAction<TContext>>>();
/// <summary>
/// The request's URI template parameters (if any).
/// </summary>
public ImmutableDictionary<string, IValueProvider<TContext, string>> TemplateParameters => GetProperty<ImmutableDictionary<string, IValueProvider<TContext, string>>>();
/// <summary>
/// The request's query parameters (if any).
/// </summary>
public ImmutableDictionary<string, IValueProvider<TContext, string>> QueryParameters => GetProperty<ImmutableDictionary<string, IValueProvider<TContext, string>>>();
#endregion // Properties
#region Invocation
/// <summary>
/// Build and configure a new HTTP request message.
/// </summary>
/// <param name="httpMethod">
/// The HTTP request method to use.
/// </param>
/// <param name="context">
/// The object used as a context for resolving deferred template values.
/// </param>
/// <param name="body">
/// Optional <see cref="HttpContent" /> representing the request body.
/// </param>
/// <param name="baseUri">
/// An optional base URI to use if the request does not already have an absolute request URI.
/// </param>
/// <returns>
/// The configured <see cref="HttpRequestMessage" />.
/// </returns>
public HttpRequestMessage BuildRequestMessage(HttpMethod httpMethod, TContext context, HttpContent body = null, Uri baseUri = null)
{
if (httpMethod == null)
throw new ArgumentNullException(nameof(httpMethod));
// Ensure we have an absolute URI.
Uri requestUri = Uri;
if (requestUri == null)
throw new InvalidOperationException("Cannot build a request message; the request does not have a URI.");
if (!requestUri.IsAbsoluteUri)
{
if (baseUri == null)
throw new InvalidOperationException("Cannot build a request message; the request does not have an absolute request URI, and no base URI was supplied.");
// Make relative to base URI.
requestUri = baseUri.AppendRelativeUri(requestUri);
}
else
{
// Extract base URI to which request URI is already (by definition) relative.
baseUri = new Uri(
requestUri.GetComponents(
UriComponents.Scheme | UriComponents.StrongAuthority,
UriFormat.UriEscaped
)
);
}
if (IsUriTemplate)
{
UriTemplate template = new UriTemplate(
requestUri.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped)
);
IDictionary<string, string> templateParameterValues = GetTemplateParameterValues(context);
requestUri = template.Populate(baseUri, templateParameterValues);
}
// Merge in any other query parameters defined directly on the request.
requestUri = MergeQueryParameters(requestUri, context);
HttpRequestMessage requestMessage = null;
try
{
requestMessage = new HttpRequestMessage(httpMethod, requestUri);
SetStandardMessageProperties(requestMessage);
if (body != null)
requestMessage.Content = body;
List<Exception> configurationActionExceptions = new List<Exception>();
foreach (RequestAction<TContext> requestAction in RequestActions)
{
if (requestAction == null)
continue;
try
{
requestAction(requestMessage, context);
}
catch (Exception eConfigurationAction)
{
configurationActionExceptions.Add(eConfigurationAction);
}
}
if (configurationActionExceptions.Count > 0)
{
throw new AggregateException(
"One or more unhandled exceptions were encountered while configuring the outgoing request message.",
configurationActionExceptions
);
}
}
catch
{
using (requestMessage)
{
throw;
}
}
return requestMessage;
}
#endregion // Invocation
#region IHttpRequest
/// <summary>
/// Actions (if any) to perform on the outgoing request message.
/// </summary>
IReadOnlyList<RequestAction<TContext>> IHttpRequestProperties<TContext>.RequestActions => RequestActions;
/// <summary>
/// Actions (if any) to perform on the outgoing request message.
/// </summary>
IReadOnlyList<ResponseAction<TContext>> IHttpRequestProperties<TContext>.ResponseActions => ResponseActions;
/// <summary>
/// The request's URI template parameters (if any).
/// </summary>
IReadOnlyDictionary<string, IValueProvider<TContext, string>> IHttpRequestProperties<TContext>.TemplateParameters => TemplateParameters;
/// <summary>
/// The request's query parameters (if any).
/// </summary>
IReadOnlyDictionary<string, IValueProvider<TContext, string>> IHttpRequestProperties<TContext>.QueryParameters => QueryParameters;
#endregion // IHttpRequest
#region Cloning
/// <summary>
/// Clone the request.
/// </summary>
/// <param name="modifications">
/// A delegate that performs modifications to the request properties.
/// </param>
/// <returns>
/// The cloned request.
/// </returns>
public new HttpRequest<TContext> Clone(Action<IDictionary<string, object>> modifications)
{
if (modifications == null)
throw new ArgumentNullException(nameof(modifications));
return (HttpRequest<TContext>)base.Clone(modifications);
}
/// <summary>
/// Create a new instance of the HTTP request using the specified properties.
/// </summary>
/// <param name="requestProperties">
/// The request properties.
/// </param>
/// <returns>
/// The new HTTP request instance.
/// </returns>
protected override HttpRequestBase CreateInstance(ImmutableDictionary<string, object> requestProperties)
{
return new HttpRequest<TContext>(requestProperties);
}
#endregion // Cloning
#region Helpers
/// <summary>
/// Merge the request's query parameters (if any) into the request URI.
/// </summary>
/// <param name="requestUri">
/// The request URI.
/// </param>
/// <param name="context">
/// The <typeparamref name="TContext"/> from which parameter values will be resolved.
/// </param>
/// <returns>
/// The request URI with query parameters merged into it.
/// </returns>
Uri MergeQueryParameters(Uri requestUri, TContext context)
{
if (requestUri == null)
throw new ArgumentNullException(nameof(requestUri));
if (QueryParameters.Count == 0)
return requestUri;
NameValueCollection queryParameters = requestUri.ParseQueryParameters();
foreach (KeyValuePair<string, IValueProvider<TContext, string>> queryParameter in QueryParameters)
{
string queryParameterValue = queryParameter.Value.Get(context);
if (queryParameterValue != null)
queryParameters[queryParameter.Key] = queryParameterValue;
else
queryParameters.Remove(queryParameter.Key);
}
return requestUri.WithQueryParameters(queryParameters);
}
/// <summary>
/// Get a dictionary mapping template parameters (if any) to their current values.
/// </summary>
/// <param name="context">
/// The <typeparamref name="TContext"/> from which parameter values will be resolved.
/// </param>
/// <returns>
/// A dictionary of key / value pairs (any parameters whose value-getters return null will be omitted).
/// </returns>
IDictionary<string, string> GetTemplateParameterValues(TContext context)
{
return
TemplateParameters.Select(templateParameter =>
{
Debug.Assert(templateParameter.Value != null);
return new
{
templateParameter.Key,
Value = templateParameter.Value.Get(context)
};
})
.Where(
templateParameter => templateParameter.Value != null
)
.ToDictionary(
templateParameter => templateParameter.Key,
templateParameter => templateParameter.Value
);
}
/// <summary>
/// Configure standard properties for the specified <see cref="HttpRequestMessage"/>.
/// </summary>
/// <param name="requestMessage">
/// The <see cref="HttpRequestMessage"/>.
/// </param>
void SetStandardMessageProperties(HttpRequestMessage requestMessage)
{
if (requestMessage == null)
throw new ArgumentNullException(nameof(requestMessage));
// TODO: Switch to HttpRequestOptions once we drop netstandard2.1 support.
#pragma warning disable CS0618 // Type or member is obsolete
requestMessage.Properties[MessageProperties.Request] = this;
#pragma warning restore CS0618 // Type or member is obsolete
}
#endregion // Helpers
}
}