Skip to content

Commit 3c494e5

Browse files
authored
Fix memory leak (#20)
* up Newtonsoft.Json version * bump version * IncludeSymbols * fix CancellationToken * async where it's possible
1 parent 0f19c1e commit 3c494e5

2 files changed

Lines changed: 83 additions & 72 deletions

File tree

src/Infobip.Api.Client/Client/ApiClient.cs

Lines changed: 76 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ public string Serialize(object obj)
8989
return JsonConvert.SerializeObject(obj, _serializerSettings);
9090
}
9191

92-
public T Deserialize<T>(HttpResponseMessage response)
92+
public async Task<T> DeserializeAsync<T>(HttpResponseMessage response)
9393
{
94-
var result = (T)Deserialize(response, typeof(T));
94+
var result = (T) await DeserializeAsync(response, typeof(T)).ConfigureAwait(false);
9595
return result;
9696
}
9797

@@ -101,17 +101,17 @@ public T Deserialize<T>(HttpResponseMessage response)
101101
/// <param name="response">The HTTP response.</param>
102102
/// <param name="type">Object type.</param>
103103
/// <returns>Object representation of the JSON string.</returns>
104-
internal object Deserialize(HttpResponseMessage response, Type type)
104+
internal async Task<object> DeserializeAsync(HttpResponseMessage response, Type type)
105105
{
106106
IList<string> headers = response.Headers.Select(x => x.Key + "=" + x.Value).ToList();
107107

108108
if (type == typeof(byte[])) // return byte array
109-
return response.Content.ReadAsByteArrayAsync().Result;
109+
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
110110

111111
// TODO: ? if (type.IsAssignableFrom(typeof(Stream)))
112112
if (type == typeof(Stream))
113113
{
114-
var bytes = response.Content.ReadAsByteArrayAsync().Result;
114+
var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
115115
if (headers != null)
116116
{
117117
var filePath = string.IsNullOrEmpty(_configuration.TempFolderPath)
@@ -137,16 +137,16 @@ internal object Deserialize(HttpResponseMessage response, Type type)
137137
}
138138

139139
if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object
140-
return DateTime.Parse(response.Content.ReadAsStringAsync().Result, null,
140+
return DateTime.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false), null,
141141
System.Globalization.DateTimeStyles.RoundtripKind);
142142

143143
if (type == typeof(string) || type.Name.StartsWith("System.Nullable")) // return primitive type
144-
return Convert.ChangeType(response.Content.ReadAsStringAsync().Result, type);
144+
return Convert.ChangeType(await response.Content.ReadAsStringAsync().ConfigureAwait(false), type);
145145

146146
// at this point, it must be a model (json)
147147
try
148148
{
149-
return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result, type,
149+
return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync().ConfigureAwait(false), type,
150150
_serializerSettings);
151151
}
152152
catch (Exception e)
@@ -157,7 +157,7 @@ internal object Deserialize(HttpResponseMessage response, Type type)
157157
}
158158

159159
/// <summary>
160-
/// Provides a default implementation of an Api client (both synchronous and asynchronous implementatios),
160+
/// Provides a default implementation of an Api client (both synchronous and asynchronous implementations),
161161
/// encapsulating general REST accessor use cases.
162162
/// </summary>
163163
/// <remarks>
@@ -393,12 +393,12 @@ private HttpRequestMessage NewRequest(
393393
return request;
394394
}
395395

396-
private ApiResponse<T> ToApiResponse<T>(HttpResponseMessage response, object responseData, Uri uri)
396+
private async Task<ApiResponse<T>> ToApiResponseAsync<T>(HttpResponseMessage response, object responseData, Uri uri)
397397
{
398398
var result = (T)responseData;
399399
string rawContent = response.Content.ToString();
400400
if (!response.IsSuccessStatusCode)
401-
rawContent = response.Content.ReadAsStringAsync().Result;
401+
rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
402402

403403
var transformed =
404404
new ApiResponse<T>(response.StatusCode, new Multimap<string, string>(), result, rawContent)
@@ -432,7 +432,7 @@ private ApiResponse<T> ToApiResponse<T>(HttpResponseMessage response, object res
432432

433433
private ApiResponse<T> Exec<T>(HttpRequestMessage req, IReadableConfiguration configuration)
434434
{
435-
return ExecAsync<T>(req, configuration).Result;
435+
return ExecAsync<T>(req, configuration).GetAwaiter().GetResult();
436436
}
437437

438438
private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
@@ -441,73 +441,82 @@ private async Task<ApiResponse<T>> ExecAsync<T>(HttpRequestMessage req,
441441
{
442442
var deserializer = new CustomJsonCodec(SerializerSettings, configuration);
443443

444+
CancellationTokenSource timeoutCancellationTokenSource = null;
444445
var finalToken = cancellationToken;
445446

446-
if (configuration.Timeout > 0)
447+
try
447448
{
448-
var tokenSource = new CancellationTokenSource(configuration.Timeout);
449-
finalToken = CancellationTokenSource.CreateLinkedTokenSource(finalToken, tokenSource.Token).Token;
450-
}
449+
if (configuration.Timeout > 0)
450+
{
451+
timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
452+
timeoutCancellationTokenSource.CancelAfter(configuration.Timeout);
453+
finalToken = timeoutCancellationTokenSource.Token;
454+
}
451455

452-
if (configuration.Proxy != null)
453-
{
454-
if (_httpClientHandler == null)
455-
throw new InvalidOperationException(
456-
"Configuration `Proxy` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
457-
_httpClientHandler.Proxy = configuration.Proxy;
458-
}
456+
if (configuration.Proxy != null)
457+
{
458+
if (_httpClientHandler == null)
459+
throw new InvalidOperationException(
460+
"Configuration `Proxy` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
461+
_httpClientHandler.Proxy = configuration.Proxy;
462+
}
459463

460-
if (configuration.ClientCertificates != null)
461-
{
462-
if (_httpClientHandler == null)
463-
throw new InvalidOperationException(
464-
"Configuration `ClientCertificates` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
465-
_httpClientHandler.ClientCertificates.AddRange(configuration.ClientCertificates);
466-
}
464+
if (configuration.ClientCertificates != null)
465+
{
466+
if (_httpClientHandler == null)
467+
throw new InvalidOperationException(
468+
"Configuration `ClientCertificates` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
469+
_httpClientHandler.ClientCertificates.AddRange(configuration.ClientCertificates);
470+
}
467471

468-
var cookieContainer = req.Properties.ContainsKey("CookieContainer")
469-
? req.Properties["CookieContainer"] as List<Cookie>
470-
: null;
472+
var cookieContainer = req.Properties.ContainsKey("CookieContainer")
473+
? req.Properties["CookieContainer"] as List<Cookie>
474+
: null;
471475

472-
if (cookieContainer != null)
473-
{
474-
if (_httpClientHandler == null)
475-
throw new InvalidOperationException(
476-
"Request property `CookieContainer` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
477-
foreach (var cookie in cookieContainer) _httpClientHandler.CookieContainer.Add(cookie);
478-
}
476+
if (cookieContainer != null)
477+
{
478+
if (_httpClientHandler == null)
479+
throw new InvalidOperationException(
480+
"Request property `CookieContainer` not supported when the client is explicitly created without an HttpClientHandler, use the proper constructor.");
481+
foreach (var cookie in cookieContainer) _httpClientHandler.CookieContainer.Add(cookie);
482+
}
479483

480-
HttpResponseMessage response;
481-
if (RetryConfiguration.AsyncRetryPolicy != null)
482-
{
483-
var policy = RetryConfiguration.AsyncRetryPolicy;
484-
var policyResult = await policy
485-
.ExecuteAndCaptureAsync(() => _httpClient.SendAsync(req, cancellationToken))
486-
.ConfigureAwait(false);
487-
response = policyResult.Outcome == OutcomeType.Successful
488-
? policyResult.Result
489-
: new HttpResponseMessage
490-
{
491-
ReasonPhrase = policyResult.FinalException.ToString(),
492-
RequestMessage = req
493-
};
494-
}
495-
else
496-
{
497-
response = await _httpClient.SendAsync(req, cancellationToken).ConfigureAwait(false);
498-
}
484+
HttpResponseMessage response;
485+
if (RetryConfiguration.AsyncRetryPolicy != null)
486+
{
487+
var policy = RetryConfiguration.AsyncRetryPolicy;
488+
var policyResult = await policy
489+
.ExecuteAndCaptureAsync(() => _httpClient.SendAsync(req, finalToken))
490+
.ConfigureAwait(false);
491+
response = policyResult.Outcome == OutcomeType.Successful
492+
? policyResult.Result
493+
: new HttpResponseMessage
494+
{
495+
ReasonPhrase = policyResult.FinalException.ToString(),
496+
RequestMessage = req
497+
};
498+
}
499+
else
500+
{
501+
response = await _httpClient.SendAsync(req, finalToken).ConfigureAwait(false);
502+
}
499503

500-
object responseData = deserializer.Deserialize<T>(response);
504+
object responseData = await deserializer.DeserializeAsync<T>(response).ConfigureAwait(false);
501505

502-
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
503-
if (typeof(AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
504-
responseData = (T)typeof(T).GetMethod("FromJson")?.Invoke(null, new object[] { response.Content });
505-
else if (typeof(T).Name == "Stream") // for binary response
506-
responseData = (T)(object)await response.Content.ReadAsStreamAsync();
506+
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
507+
if (typeof(AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
508+
responseData = (T)typeof(T).GetMethod("FromJson")?.Invoke(null, new object[] { response.Content });
509+
else if (typeof(T).Name == "Stream") // for binary response
510+
responseData = (T)(object)await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
507511

508-
var result = ToApiResponse<T>(response, responseData, req.RequestUri);
512+
var result = await ToApiResponseAsync<T>(response, responseData, req.RequestUri).ConfigureAwait(false);
509513

510-
return result;
514+
return result;
515+
}
516+
finally
517+
{
518+
timeoutCancellationTokenSource?.Dispose();
519+
}
511520
}
512521

513522
#region IAsynchronousClient

src/Infobip.Api.Client/Infobip.Api.Client.csproj

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,25 @@
99
<Authors>Infobip</Authors>
1010
<Company>Infobip</Company>
1111
<AssemblyTitle>Infobip.Api.Client</AssemblyTitle>
12-
<Description>C# library for consuming Infobip&#39;s API</Description>
12+
<Description>C# library for consuming Infobip's API</Description>
1313
<Copyright>Copyright @ Infobip ltd. All rights reserved.</Copyright>
1414
<RootNamespace>Infobip.Api.Client</RootNamespace>
15-
<Version>2.1.0</Version>
16-
<FileVersion>2.1.0</FileVersion>
17-
<AssemblyVersion>2.1.0</AssemblyVersion>
15+
<Version>2.2.0</Version>
16+
<FileVersion>2.2.0</FileVersion>
17+
<AssemblyVersion>2.2.0</AssemblyVersion>
1818
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\Infobip.Api.Client.xml</DocumentationFile>
1919
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2020
<RepositoryUrl>https://github.com/infobip/infobip-api-csharp-client.git</RepositoryUrl>
2121
<RepositoryType>git</RepositoryType>
2222
<PackageReleaseNotes>Support for Infobip Email API included in library</PackageReleaseNotes>
2323
<PackageTags>Infobip API SMS TFA 2FA Email dotnet .NET Core C# CSharp client</PackageTags>
2424
<PackageIcon>icon.png</PackageIcon>
25+
<IncludeSymbols>true</IncludeSymbols>
26+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
2527
</PropertyGroup>
2628

2729
<ItemGroup>
28-
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
30+
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
2931
<PackageReference Include="Polly" Version="7.2.1" />
3032
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
3133
</ItemGroup>

0 commit comments

Comments
 (0)