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>
0 commit comments