Skip to content

Commit d6c2349

Browse files
akunzaithinkingserious
authored andcommitted
Refactoring SendGridClient to support inject external managed HttpClient (#839)
Fix issue #670
1 parent 2310b6e commit d6c2349

File tree

2 files changed

+111
-113
lines changed

2 files changed

+111
-113
lines changed

src/SendGrid/SendGridClient.cs

Lines changed: 98 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// <copyright file="SendGridClient.cs" company="Twilio SendGrid">
1+
// <copyright file="SendGridClient.cs" company="Twilio SendGrid">
22
// Copyright (c) Twilio SendGrid. All rights reserved.
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
// </copyright>
@@ -24,12 +24,29 @@ namespace SendGrid
2424
/// </summary>
2525
public class SendGridClient : ISendGridClient
2626
{
27-
private readonly SendGridClientOptions options = new SendGridClientOptions();
27+
private const string Scheme = "Bearer";
28+
private const string ContentType = "Content-Type";
29+
private const string DefaultMediaType = "application/json";
30+
31+
/// <summary>
32+
/// The <see cref="SendGridClient"/> assembly version to send in request User-Agent header
33+
/// </summary>
34+
private static readonly string ClientVersion = typeof(SendGridClient).GetTypeInfo().Assembly.GetName().Version.ToString();
35+
36+
/// <summary>
37+
/// The default configuration settings to use with the SendGrid client
38+
/// </summary>
39+
private static readonly SendGridClientOptions DefaultOptions = new SendGridClientOptions();
40+
41+
/// <summary>
42+
/// The configuration to use with current <see cref="SendGridClient"/> instance
43+
/// </summary>
44+
private readonly SendGridClientOptions options;
2845

2946
/// <summary>
3047
/// The HttpClient instance to use for all calls from this SendGridClient instance.
3148
/// </summary>
32-
private HttpClient client;
49+
private readonly HttpClient client;
3350

3451
/// <summary>
3552
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
@@ -42,27 +59,8 @@ public class SendGridClient : ISendGridClient
4259
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
4360
/// <returns>Interface to the Twilio SendGrid REST API</returns>
4461
public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
62+
: this(CreateHttpClientWithWebProxy(webProxy), new SendGridClientOptions { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath })
4563
{
46-
// Create client with WebProxy if set
47-
if (webProxy != null)
48-
{
49-
var httpClientHandler = new HttpClientHandler()
50-
{
51-
Proxy = webProxy,
52-
PreAuthenticate = true,
53-
UseDefaultCredentials = false,
54-
};
55-
56-
var retryHandler = new RetryDelegatingHandler(httpClientHandler, this.options.ReliabilitySettings);
57-
58-
this.client = new HttpClient(retryHandler);
59-
}
60-
else
61-
{
62-
this.client = this.CreateHttpClientWithRetryHandler();
63-
}
64-
65-
this.InitiateClient(apiKey, host, requestHeaders, version, urlPath);
6664
}
6765

6866
/// <summary>
@@ -86,7 +84,7 @@ public SendGridClient(SendGridClientOptions options)
8684
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
8785
/// <returns>Interface to the Twilio SendGrid REST API</returns>
8886
public SendGridClient(HttpClient httpClient, string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
89-
: this(httpClient, new SendGridClientOptions() { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath })
87+
: this(httpClient, new SendGridClientOptions { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath })
9088
{
9189
}
9290

@@ -100,27 +98,24 @@ public SendGridClient(HttpClient httpClient, string apiKey, string host = null,
10098
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
10199
/// <returns>Interface to the Twilio SendGrid REST API</returns>
102100
public SendGridClient(string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
103-
: this(httpClient: null, apiKey: apiKey, host: host, requestHeaders: requestHeaders, version: version, urlPath: urlPath)
101+
: this(null, new SendGridClientOptions { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath })
104102
{
105103
}
106104

107105
/// <summary>
108106
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
109107
/// </summary>
110-
/// <param name="httpClient">An optional http client which may me injected in order to facilitate testing.</param>
108+
/// <param name="httpClient">An optional HTTP client which may me injected in order to facilitate testing.</param>
111109
/// <param name="options">A <see cref="SendGridClientOptions"/> instance that defines the configuration settings to use with the client </param>
112110
/// <returns>Interface to the Twilio SendGrid REST API</returns>
113111
internal SendGridClient(HttpClient httpClient, SendGridClientOptions options)
114112
{
115-
if (options == null)
113+
this.options = options ?? throw new ArgumentNullException(nameof(options));
114+
this.client = httpClient ?? CreateHttpClientWithRetryHandler();
115+
if (this.options.RequestHeaders != null && this.options.RequestHeaders.TryGetValue(ContentType, out var contentType))
116116
{
117-
throw new ArgumentNullException(nameof(options));
117+
this.MediaType = contentType;
118118
}
119-
120-
this.options = options;
121-
this.client = (httpClient == null) ? this.CreateHttpClientWithRetryHandler() : httpClient;
122-
123-
this.InitiateClient(options.ApiKey, options.Host, options.RequestHeaders, options.Version, options.UrlPath);
124119
}
125120

126121
/// <summary>
@@ -157,17 +152,25 @@ public enum Method
157152
/// <summary>
158153
/// Gets or sets the path to the API resource.
159154
/// </summary>
160-
public string UrlPath { get; set; }
155+
public string UrlPath
156+
{
157+
get => this.options.UrlPath;
158+
set => this.options.UrlPath = value;
159+
}
161160

162161
/// <summary>
163162
/// Gets or sets the API version.
164163
/// </summary>
165-
public string Version { get; set; }
164+
public string Version
165+
{
166+
get => this.options.Version;
167+
set => this.options.Version = value;
168+
}
166169

167170
/// <summary>
168171
/// Gets or sets the request media type.
169172
/// </summary>
170-
public string MediaType { get; set; }
173+
public string MediaType { get; set; } = DefaultMediaType;
171174

172175
/// <summary>
173176
/// Add the authorization header, override to customize
@@ -197,37 +200,48 @@ public virtual AuthenticationHeaderValue AddAuthorization(KeyValuePair<string, s
197200
/// </summary>
198201
/// <param name="method">HTTP verb</param>
199202
/// <param name="requestBody">JSON formatted string</param>
200-
/// <param name="queryParams">JSON formatted query paramaters</param>
203+
/// <param name="queryParams">JSON formatted query parameters</param>
201204
/// <param name="urlPath">The path to the API endpoint.</param>
202205
/// <param name="cancellationToken">Cancel the asynchronous call.</param>
203206
/// <returns>Response object</returns>
204207
/// <exception cref="Exception">The method will NOT catch and swallow exceptions generated by sending a request
205-
/// through the internal http client. Any underlying exception will pass right through.
208+
/// through the internal HTTP client. Any underlying exception will pass right through.
206209
/// In particular, this means that you may expect
207-
/// a TimeoutException if you are not connected to the internet.</exception>
210+
/// a TimeoutException if you are not connected to the Internet.</exception>
208211
public async Task<Response> RequestAsync(
209212
SendGridClient.Method method,
210213
string requestBody = null,
211214
string queryParams = null,
212215
string urlPath = null,
213216
CancellationToken cancellationToken = default(CancellationToken))
214217
{
215-
var endpoint = this.client.BaseAddress + this.BuildUrl(urlPath, queryParams);
216-
217-
// Build the request body
218-
StringContent content = null;
219-
if (requestBody != null)
218+
var baseAddress = new Uri(string.IsNullOrWhiteSpace(this.options.Host) ? DefaultOptions.Host : this.options.Host);
219+
if (!baseAddress.OriginalString.EndsWith("/"))
220220
{
221-
content = new StringContent(requestBody, Encoding.UTF8, this.MediaType);
221+
baseAddress = new Uri(baseAddress.OriginalString + "/");
222222
}
223223

224224
// Build the final request
225225
var request = new HttpRequestMessage
226226
{
227227
Method = new HttpMethod(method.ToString()),
228-
RequestUri = new Uri(endpoint),
229-
Content = content
228+
RequestUri = new Uri(baseAddress, this.BuildUrl(urlPath, queryParams)),
229+
Content = requestBody == null ? null : new StringContent(requestBody, Encoding.UTF8, this.MediaType)
230230
};
231+
232+
// set header overrides
233+
if (this.options.RequestHeaders?.Count > 0)
234+
{
235+
foreach (var header in this.options.RequestHeaders)
236+
{
237+
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
238+
}
239+
}
240+
241+
// set standard headers
242+
request.Headers.Authorization = new AuthenticationHeaderValue(Scheme, this.options.ApiKey);
243+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(this.MediaType));
244+
request.Headers.UserAgent.TryParseAdd($"sendgrid/{ClientVersion} csharp");
231245
return await this.MakeRequest(request, cancellationToken).ConfigureAwait(false);
232246
}
233247

@@ -246,6 +260,37 @@ public async Task<Response> RequestAsync(
246260
cancellationToken: cancellationToken).ConfigureAwait(false);
247261
}
248262

263+
private static HttpClient CreateHttpClientWithRetryHandler()
264+
{
265+
return new HttpClient(new RetryDelegatingHandler(DefaultOptions.ReliabilitySettings));
266+
}
267+
268+
/// <summary>
269+
/// Create client with WebProxy if set
270+
/// </summary>
271+
/// <param name="webProxy">the WebProxy</param>
272+
/// <returns>HttpClient with RetryDelegatingHandler and WebProxy if set</returns>
273+
private static HttpClient CreateHttpClientWithWebProxy(IWebProxy webProxy)
274+
{
275+
if (webProxy != null)
276+
{
277+
var httpClientHandler = new HttpClientHandler()
278+
{
279+
Proxy = webProxy,
280+
PreAuthenticate = true,
281+
UseDefaultCredentials = false,
282+
};
283+
284+
var retryHandler = new RetryDelegatingHandler(httpClientHandler, DefaultOptions.ReliabilitySettings);
285+
286+
return new HttpClient(retryHandler);
287+
}
288+
else
289+
{
290+
return CreateHttpClientWithRetryHandler();
291+
}
292+
}
293+
249294
/// <summary>
250295
/// Build the final URL
251296
/// </summary>
@@ -258,12 +303,12 @@ private string BuildUrl(string urlPath, string queryParams = null)
258303
{
259304
string url = null;
260305

261-
// create urlPAth - from parameter if overridden on call or from ctor parameter
262-
var urlpath = urlPath ?? this.UrlPath;
306+
// create urlPAth - from parameter if overridden on call or from constructor parameter
307+
var urlpath = urlPath ?? this.options.UrlPath;
263308

264-
if (this.Version != null)
309+
if (this.options.Version != null)
265310
{
266-
url = this.Version + "/" + urlpath;
311+
url = this.options.Version + "/" + urlpath;
267312
}
268313
else
269314
{
@@ -280,79 +325,19 @@ private string BuildUrl(string urlPath, string queryParams = null)
280325
{
281326
if (query != "?")
282327
{
283-
query = query + "&";
328+
query += "&";
284329
}
285330

286331
query = query + pair.Key + "=" + element;
287332
}
288333
}
289334

290-
url = url + query;
335+
url += query;
291336
}
292337

293338
return url;
294339
}
295340

296-
private HttpClient CreateHttpClientWithRetryHandler()
297-
{
298-
return new HttpClient(new RetryDelegatingHandler(this.options.ReliabilitySettings));
299-
}
300-
301-
/// <summary>
302-
/// Common method to initiate internal fields regardless of which constructor was used.
303-
/// </summary>
304-
/// <param name="apiKey">Your Twilio SendGrid API key.</param>
305-
/// <param name="host">Base url (e.g. https://api.sendgrid.com)</param>
306-
/// <param name="requestHeaders">A dictionary of request headers</param>
307-
/// <param name="version">API version, override AddVersion to customize</param>
308-
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
309-
private void InitiateClient(string apiKey, string host, Dictionary<string, string> requestHeaders, string version, string urlPath)
310-
{
311-
this.UrlPath = urlPath;
312-
this.Version = version;
313-
314-
var baseAddress = host ?? "https://api.sendgrid.com";
315-
var clientVersion = this.GetType().GetTypeInfo().Assembly.GetName().Version.ToString();
316-
317-
// standard headers
318-
this.client.BaseAddress = new Uri(baseAddress);
319-
Dictionary<string, string> headers = new Dictionary<string, string>
320-
{
321-
{ "Authorization", "Bearer " + apiKey },
322-
{ "Content-Type", "application/json" },
323-
{ "User-Agent", "sendgrid/" + clientVersion + " csharp" },
324-
{ "Accept", "application/json" }
325-
};
326-
327-
// set header overrides
328-
if (requestHeaders != null)
329-
{
330-
foreach (var header in requestHeaders)
331-
{
332-
headers[header.Key] = header.Value;
333-
}
334-
}
335-
336-
// add headers to httpClient
337-
foreach (var header in headers)
338-
{
339-
if (header.Key == "Authorization")
340-
{
341-
var split = header.Value.Split();
342-
this.client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(split[0], split[1]);
343-
}
344-
else if (header.Key == "Content-Type")
345-
{
346-
this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(header.Value));
347-
this.MediaType = header.Value;
348-
}
349-
else
350-
{
351-
this.client.DefaultRequestHeaders.Add(header.Key, header.Value);
352-
}
353-
}
354-
}
355-
356341
/// <summary>
357342
/// Parses a JSON string without removing duplicate keys.
358343
/// </summary>

tests/SendGrid.Tests/Integration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6125,6 +6125,19 @@ public async Task TestRetryBehaviourSucceedsOnSecondAttempt()
61256125

61266126
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
61276127
}
6128+
6129+
/// <summary>
6130+
/// Tests the conditions in issue #670.
6131+
/// </summary>
6132+
/// <returns></returns>
6133+
[Fact]
6134+
public void TestInjectSameHttpClientWithMultipleInstance()
6135+
{
6136+
var httpMessageHandler = new FixedStatusAndMessageHttpMessageHandler(HttpStatusCode.Accepted, string.Empty);
6137+
var clientToInject = new HttpClient(httpMessageHandler);
6138+
var sg1 = new SendGridClient(clientToInject, fixture.apiKey);
6139+
var sg2 = new SendGridClient(clientToInject, fixture.apiKey);
6140+
}
61286141
}
61296142

61306143
public class FakeWebProxy : IWebProxy

0 commit comments

Comments
 (0)