-
Notifications
You must be signed in to change notification settings - Fork 24
/
RequestInformation.cs
379 lines (367 loc) · 18.1 KB
/
RequestInformation.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
376
377
378
379
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
namespace Microsoft.Kiota.Abstractions
{
/// <summary>
/// This class represents an abstract HTTP request.
/// </summary>
public class RequestInformation
{
/// <summary>
/// Creates a new instance of <see cref="RequestInformation"/>.
/// </summary>
public RequestInformation()
{
}
/// <summary>
/// Creates a new instance of <see cref="RequestInformation"/> with the given method and url template.
/// </summary>
/// <param name="method"></param>
/// <param name="urlTemplate"></param>
/// <param name="pathParameters"></param>
public RequestInformation(Method method, string urlTemplate, IDictionary<string, object> pathParameters)
{
HttpMethod = method;
UrlTemplate = urlTemplate;
PathParameters = pathParameters;
}
/// <summary>
/// Configures the current request configuration headers, query parameters, and options base on the callback provided.
/// </summary>
/// <typeparam name="T">Type for the query parameters</typeparam>
/// <param name="requestConfiguration">Callback to configure the request</param>
public void Configure<T>(Action<RequestConfiguration<T>>? requestConfiguration) where T : class, new()
{
if(requestConfiguration == null) return;
var requestConfig = new RequestConfiguration<T>();
requestConfiguration(requestConfig);
AddQueryParameters(requestConfig.QueryParameters);
AddRequestOptions(requestConfig.Options);
AddHeaders(requestConfig.Headers);
}
internal const string RawUrlKey = "request-raw-url";
private Uri? _rawUri;
/// <summary>
/// The URI of the request.
/// </summary>
public Uri URI
{
set
{
if(value == null)
throw new ArgumentNullException(nameof(value));
QueryParameters.Clear();
PathParameters.Clear();
_rawUri = value;
}
get
{
if(_rawUri != null)
return _rawUri;
else if(PathParameters.TryGetValue(RawUrlKey, out var rawUrl) &&
rawUrl is string rawUrlString)
{
URI = new Uri(rawUrlString);
return _rawUri!;
}
else
{
if(UrlTemplate?.IndexOf("{+baseurl}", StringComparison.OrdinalIgnoreCase) >= 0 && !PathParameters.ContainsKey("baseurl"))
throw new InvalidOperationException($"{nameof(PathParameters)} must contain a value for \"baseurl\" for the url to be built.");
var substitutions = new Dictionary<string, object>();
foreach(var urlTemplateParameter in PathParameters)
{
substitutions.Add(urlTemplateParameter.Key, GetSanitizedValue(urlTemplateParameter.Value));
}
foreach(var queryStringParameter in QueryParameters)
{
if(queryStringParameter.Value != null)
{
substitutions.Add(queryStringParameter.Key, GetSanitizedValue(queryStringParameter.Value));
}
}
return new Uri(Std.UriTemplate.Expand(UrlTemplate, substitutions));
}
}
}
/// <summary>
/// Sanitizes objects in order to appear appropiately in the URL
/// </summary>
/// <param name="value">Object to be sanitized</param>
/// <returns>Sanitized object</returns>
private static object GetSanitizedValue(object value) => value switch
{
bool boolean => boolean.ToString().ToLower(),// pass in a lowercase string as the final url will be uppercase due to the way ToString() works for booleans
DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("o"),// Default to ISO 8601 for datetimeoffsets in the url.
DateTime dateTime => dateTime.ToString("o"),// Default to ISO 8601 for datetimes in the url.
Guid guid => guid.ToString("D"),// Default of 32 digits separated by hyphens
Date date => date.ToString(), //Default to string format of the custom date object
Time time => time.ToString(), //Default to string format of the custom time object
_ => value,//return object as is as the ToString method is good enough.
};
/// <summary>
/// The Url template for the current request.
/// </summary>
public string? UrlTemplate { get; set; }
/// <summary>
/// The path parameters to use for the URL template when generating the URI.
/// </summary>
public IDictionary<string, object> PathParameters { get; set; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The <see cref="Method">HTTP method</see> of the request.
/// </summary>
public Method HttpMethod { get; set; }
/// <summary>
/// The Query Parameters of the request.
/// </summary>
public IDictionary<string, object> QueryParameters { get; set; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Vanity method to add the query parameters to the request query parameters dictionary.
/// </summary>
/// <param name="source">The query parameters to add.</param>
#if NET5_0_OR_GREATER
public void AddQueryParameters<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T source)
#else
public void AddQueryParameters<T>(T source)
#endif
{
if(source == null) return;
foreach(var property in typeof(T)
.GetProperties()
.Select(
x => (
Name: x.GetCustomAttributes(false)
.OfType<QueryParameterAttribute>()
.FirstOrDefault()?.TemplateName ?? x.Name,
Value: x.GetValue(source)
)
)
.Where(x => x.Value != null &&
!QueryParameters.ContainsKey(x.Name!) &&
!string.IsNullOrEmpty(x.Value.ToString()) && // no need to add an empty string value
(x.Value is not ICollection collection || collection.Count > 0))) // no need to add empty collection
{
QueryParameters.AddOrReplace(property.Name!, property.Value!);
}
}
/// <summary>
/// The Request Headers.
/// </summary>
public RequestHeaders Headers { get; private set; } = new();
/// <summary>
/// Vanity method to add the headers to the request headers dictionary.
/// </summary>
public void AddHeaders(RequestHeaders headers)
{
if(headers == null) return;
Headers.AddAll(headers);
}
/// <summary>
/// The Request Body.
/// </summary>
public Stream Content { get; set; } = Stream.Null;
private readonly Dictionary<string, IRequestOption> _requestOptions = new Dictionary<string, IRequestOption>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the options for this request. Options are unique by type. If an option of the same type is added twice, the last one wins.
/// </summary>
public IEnumerable<IRequestOption> RequestOptions { get { return _requestOptions.Values; } }
/// <summary>
/// Adds an option to the request.
/// </summary>
/// <param name="options">The option to add.</param>
public void AddRequestOptions(IEnumerable<IRequestOption> options)
{
if(options == null) return;
foreach(var option in options.Where(x => x != null))
_requestOptions.AddOrReplace(option.GetType().FullName!, option);
}
/// <summary>
/// Removes given options from the current request.
/// </summary>
/// <param name="options">Options to remove.</param>
public void RemoveRequestOptions(params IRequestOption[] options)
{
if(!options?.Any() ?? false) throw new ArgumentNullException(nameof(options));
foreach(var optionName in options!.Where(x => x != null).Select(x => x.GetType().FullName))
_requestOptions.Remove(optionName!);
}
/// <summary>
/// Gets a <see cref="IRequestOption"/> instance of the matching type.
/// </summary>
public T? GetRequestOption<T>() => _requestOptions.TryGetValue(typeof(T).FullName!, out var requestOption) ? (T)requestOption : default;
/// <summary>
/// Adds a <see cref="IResponseHandler"/> as a <see cref="IRequestOption"/> for the request.
/// </summary>
public void SetResponseHandler(IResponseHandler responseHandler)
{
if(responseHandler == null)
throw new ArgumentNullException(nameof(responseHandler));
var responseHandlerOption = new ResponseHandlerOption
{
ResponseHandler = responseHandler
};
AddRequestOptions(new[] { responseHandlerOption });
}
private const string BinaryContentType = "application/octet-stream";
private const string ContentTypeHeader = "Content-Type";
/// <summary>
/// Sets the request body to a binary stream.
/// </summary>
/// <param name="content">The binary stream to set as a body.</param>
[Obsolete("Use SetStreamContent and pass the content type instead")]
public void SetStreamContent(Stream content) => SetStreamContent(content, BinaryContentType);
/// <summary>
/// Sets the request body to a binary stream.
/// </summary>
/// <param name="content">The binary stream to set as a body.</param>
/// <param name="contentType">The content type to set.</param>
public void SetStreamContent(Stream content, string contentType)
{
using var activity = _activitySource?.StartActivity(nameof(SetStreamContent));
SetRequestType(content, activity);
Content = content;
Headers.TryAdd(ContentTypeHeader, contentType);
}
private static ActivitySource _activitySource = new(typeof(RequestInformation).Namespace!);
/// <summary>
/// Sets the request body from a model with the specified content type.
/// </summary>
/// <param name="requestAdapter">The core service to get the serialization writer from.</param>
/// <param name="items">The models to serialize.</param>
/// <param name="contentType">The content type to set.</param>
/// <typeparam name="T">The model type to serialize.</typeparam>
public void SetContentFromParsable<T>(IRequestAdapter requestAdapter, string contentType, IEnumerable<T> items) where T : IParsable
{
using var activity = _activitySource?.StartActivity(nameof(SetContentFromParsable));
using var writer = GetSerializationWriter(requestAdapter, contentType, items);
SetRequestType(items.FirstOrDefault(static x => x != null), activity);
writer.WriteCollectionOfObjectValues(null, items);
Headers.TryAdd(ContentTypeHeader, contentType);
Content = writer.GetSerializedContent();
}
/// <summary>
/// Sets the request body from a model with the specified content type.
/// </summary>
/// <param name="requestAdapter">The core service to get the serialization writer from.</param>
/// <param name="item">The model to serialize.</param>
/// <param name="contentType">The content type to set.</param>
/// <typeparam name="T">The model type to serialize.</typeparam>
public void SetContentFromParsable<T>(IRequestAdapter requestAdapter, string contentType, T item) where T : IParsable
{
using var activity = _activitySource?.StartActivity(nameof(SetContentFromParsable));
using var writer = GetSerializationWriter(requestAdapter, contentType, item);
SetRequestType(item, activity);
if(item is MultipartBody mpBody)
{
contentType += "; boundary=" + mpBody.Boundary;
mpBody.RequestAdapter = requestAdapter;
}
writer.WriteObjectValue(null, item);
Headers.TryAdd(ContentTypeHeader, contentType);
Content = writer.GetSerializedContent();
}
private static void SetRequestType(object? result, Activity? activity)
{
if(activity == null) return;
if(result == null) return;
activity.SetTag("com.microsoft.kiota.request.type", result.GetType().FullName);
}
private static ISerializationWriter GetSerializationWriter<T>(IRequestAdapter requestAdapter, string contentType, T item)
{
if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType));
if(requestAdapter == null) throw new ArgumentNullException(nameof(requestAdapter));
if(item == null) throw new InvalidOperationException($"{nameof(item)} cannot be null");
return requestAdapter.SerializationWriterFactory.GetSerializationWriter(contentType);
}
/// <summary>
/// Sets the request body from a scalar value with the specified content type.
/// </summary>
/// <param name="requestAdapter">The core service to get the serialization writer from.</param>
/// <param name="items">The scalar values to serialize.</param>
/// <param name="contentType">The content type to set.</param>
/// <typeparam name="T">The model type to serialize.</typeparam>
public void SetContentFromScalarCollection<T>(IRequestAdapter requestAdapter, string contentType, IEnumerable<T> items)
{
using var activity = _activitySource?.StartActivity(nameof(SetContentFromScalarCollection));
using var writer = GetSerializationWriter(requestAdapter, contentType, items);
SetRequestType(items.FirstOrDefault(static x => x != null), activity);
writer.WriteCollectionOfPrimitiveValues(null, items);
Headers.TryAdd(ContentTypeHeader, contentType);
Content = writer.GetSerializedContent();
}
/// <summary>
/// Sets the request body from a scalar value with the specified content type.
/// </summary>
/// <param name="requestAdapter">The core service to get the serialization writer from.</param>
/// <param name="item">The scalar value to serialize.</param>
/// <param name="contentType">The content type to set.</param>
/// <typeparam name="T">The model type to serialize.</typeparam>
public void SetContentFromScalar<T>(IRequestAdapter requestAdapter, string contentType, T item)
{
using var activity = _activitySource?.StartActivity(nameof(SetContentFromScalar));
using var writer = GetSerializationWriter(requestAdapter, contentType, item);
SetRequestType(item, activity);
switch(item)
{
case string s:
writer.WriteStringValue(null, s);
break;
case bool b:
writer.WriteBoolValue(null, b);
break;
case byte b:
writer.WriteByteValue(null, b);
break;
case sbyte b:
writer.WriteSbyteValue(null, b);
break;
case int i:
writer.WriteIntValue(null, i);
break;
case float f:
writer.WriteFloatValue(null, f);
break;
case long l:
writer.WriteLongValue(null, l);
break;
case double d:
writer.WriteDoubleValue(null, d);
break;
case Guid g:
writer.WriteGuidValue(null, g);
break;
case DateTimeOffset dto:
writer.WriteDateTimeOffsetValue(null, dto);
break;
case TimeSpan timeSpan:
writer.WriteTimeSpanValue(null, timeSpan);
break;
case Date date:
writer.WriteDateValue(null, date);
break;
case Time time:
writer.WriteTimeValue(null, time);
break;
case null:
writer.WriteNullValue(null);
break;
default:
throw new InvalidOperationException($"error serialization data value with unknown type {item?.GetType()}");
}
Headers.TryAdd(ContentTypeHeader, contentType);
Content = writer.GetSerializedContent();
}
}
}