Skip to content

Commit

Permalink
Add support for .NET 5 (Azure#2169)
Browse files Browse the repository at this point in the history
* feat(all): Add support for .NET 5.0

* refactor(all): Update APIs to follow .NET 5.0 patterns

* refactor(e2e-tests): Update E2E tests to follow .NET 5.0 patterns

* refactor(all): Simplify disposal of IDisposable X509Certificate

* fix(vsts): Update the location for log analytics workspace resource

* refactor(e2e-tests): Dispose IDisposable types

* fix(e2e-tests): Dispose IDisposable types created - hmac
  • Loading branch information
abhipsaMisra authored Sep 15, 2021
1 parent 688e6f0 commit 4b6752e
Show file tree
Hide file tree
Showing 91 changed files with 967 additions and 708 deletions.
65 changes: 65 additions & 0 deletions common/src/HttpContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Azure.Devices.Shared
{
/// <summary>
/// Extensions added to simplify the usage of <see cref="HttpContent"/> APIs based on the .NET implementation used.
/// </summary>
internal static class HttpContentExtensions
{
internal static async Task CopyToStreamAsync(this HttpContent content, Stream stream, CancellationToken cancellationToken)
{
#if NET5_0
await content.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
#else
// .NET implementations < .NET 5.0 do not support CancellationTokens for HttpContent APIs, so we will discard it.
_ = cancellationToken;

await content.CopyToAsync(stream).ConfigureAwait(false);
#endif
}

internal static Task<Stream> ReadHttpContentAsStream(this HttpContent httpContent, CancellationToken cancellationToken)
{
#if NET5_0
return httpContent.ReadAsStreamAsync(cancellationToken);
#else
// .NET implementations < .NET 5.0 do not support CancellationTokens for HttpContent APIs, so we will discard it.
_ = cancellationToken;

return httpContent.ReadAsStreamAsync();
#endif
}

internal static Task<byte[]> ReadHttpContentAsByteArrayAsync(this HttpContent content, CancellationToken cancellationToken)
{
#if NET5_0
return content.ReadAsByteArrayAsync(cancellationToken);
#else
// .NET implementations < .NET 5.0 do not support CancellationTokens for HttpContent APIs, so we will discard it.
_ = cancellationToken;

return content.ReadAsByteArrayAsync();
#endif
}

internal static Task<string> ReadHttpContentAsStringAsync(this HttpContent content, CancellationToken cancellationToken)
{
#if NET5_0
return content.ReadAsStringAsync(cancellationToken);
#else
// .NET implementations < .NET 5.0 do not support CancellationTokens for HttpContent APIs, so we will discard it.
_ = cancellationToken;

return content.ReadAsStringAsync();
#endif
}

}
}
35 changes: 35 additions & 0 deletions common/src/HttpMessageHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Net.Http;
using System.Text;
using Newtonsoft.Json;

#if NET451
using System.Net.Http.Formatting;
#endif

namespace Microsoft.Azure.Devices.Shared
{
/// <summary>
/// A helper class to simplify operations with Http messages based on the .NET implementation used.
/// </summary>
internal static class HttpMessageHelper
{
#if NET451
private static readonly JsonMediaTypeFormatter s_jsonFormatter = new JsonMediaTypeFormatter();
#else
private const string ApplicationJson = "application/json";
#endif

internal static void SetHttpRequestMessageContent<T>(HttpRequestMessage requestMessage, T entity)
{
#if NET451
requestMessage.Content = new ObjectContent<T>(entity, s_jsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMessage.Content = new StringContent(str, Encoding.UTF8, ApplicationJson);
#endif
}
}
}
24 changes: 24 additions & 0 deletions common/src/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Azure.Devices.Shared
{
/// <summary>
/// Extensions added to simplify the usage of <see cref="Stream"/> APIs based on the .NET implementation used.
/// </summary>
internal static class StreamExtensions
{
internal static async Task WriteToStreamAsync(this Stream stream, byte[] requestBytes, CancellationToken cancellationToken)
{
#if NET451 || NET472 || NETSTANDARD2_0
await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken).ConfigureAwait(false);
#else
await stream.WriteAsync(requestBytes, cancellationToken).ConfigureAwait(false);
#endif
}
}
}
27 changes: 27 additions & 0 deletions common/src/TaskCompletionSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading.Tasks;

namespace Microsoft.Azure.Devices.Shared
{
/// <summary>
/// A <see cref="TaskCompletionSource{boolean}"/> implementation that returns a <see cref="Task"/> when completed.
/// </summary>
/// <remarks>
/// Represents the producer side of a <see cref="Task"/> unbound to a delegate, providing access to the consumer side through the <see cref="Task"/> property.
/// This is used for .NET implementations lower than .NET 5.0, which lack a native implementation of the non-generic TaskCompletionSource.
/// </remarks>
internal sealed class TaskCompletionSource : TaskCompletionSource<bool>
{
public TaskCompletionSource()
{
}

public bool TrySetResult() => TrySetResult(true);

public void SetResult() => SetResult(true);

public override string ToString() => $"TaskCompletionSource[status: {Task.Status}]";
}
}
59 changes: 14 additions & 45 deletions common/src/service/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Common;
Expand All @@ -19,19 +18,13 @@
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;

#if NET451
using System.Net.Http.Formatting;
#endif
using static Microsoft.Azure.Devices.Shared.HttpMessageHelper;

namespace Microsoft.Azure.Devices
{
internal sealed class HttpClientHelper : IHttpClientHelper
{
private const string ApplicationJson = "application/json";

#if NET451
static readonly JsonMediaTypeFormatter JsonFormatter = new JsonMediaTypeFormatter();
#endif
private readonly Uri _baseAddress;
private readonly IAuthorizationHeaderProvider _authenticationHeaderProvider;
private readonly IReadOnlyDictionary<HttpStatusCode, Func<HttpResponseMessage, Task<Exception>>> _defaultErrorMapping;
Expand Down Expand Up @@ -179,12 +172,8 @@ await ExecuteAsync(
(requestMsg, token) =>
{
InsertEtag(requestMsg, entity, operationType);
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent(requestMsg, entity);

return Task.FromResult(0);
},
async (httpClient, token) => result = await ReadResponseMessageAsync<T>(httpClient, token).ConfigureAwait(false),
Expand All @@ -208,12 +197,7 @@ await ExecuteAsync(
new Uri(_baseAddress, requestUri),
(requestMsg, token) =>
{
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, System.Text.Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent<T>(requestMsg, entity);
return Task.FromResult(0);
},
async (httpClient, token) => result = await ReadResponseMessageAsync<T2>(httpClient, token).ConfigureAwait(false),
Expand All @@ -237,12 +221,8 @@ await ExecuteAsync(
(requestMsg, token) =>
{
InsertEtag(requestMsg, etag, operationType);
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = Newtonsoft.Json.JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, System.Text.Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent<T>(requestMsg, entity);

return Task.FromResult(0);
},
null,
Expand All @@ -267,12 +247,8 @@ await ExecuteAsync(
{
// TODO: skintali: Use string etag when service side changes are ready
InsertEtag(requestMsg, etag, operationType);
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent<T>(requestMsg, entity);

return Task.FromResult(0);
},
async (httpClient, token) => result = await ReadResponseMessageAsync<T2>(httpClient, token).ConfigureAwait(false),
Expand All @@ -291,12 +267,8 @@ await ExecuteAsync(
(requestMsg, token) =>
{
InsertEtag(requestMsg, etag, PutOperationType.UpdateEntity);
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent<T>(requestMsg, entity);

return Task.FromResult(0);
},
null,
Expand All @@ -317,12 +289,8 @@ await ExecuteAsync(
(requestMsg, token) =>
{
InsertEtag(requestMsg, etag, putOperationType);
#if NET451
requestMsg.Content = new ObjectContent<T>(entity, JsonFormatter);
#else
string str = JsonConvert.SerializeObject(entity);
requestMsg.Content = new StringContent(str, System.Text.Encoding.UTF8, ApplicationJson);
#endif
SetHttpRequestMessageContent<T>(requestMsg, entity);

return Task.FromResult(0);
},
async (httpClient, token) => result = await ReadResponseMessageAsync<T2>(httpClient, token).ConfigureAwait(false),
Expand All @@ -342,9 +310,10 @@ private static async Task<T> ReadResponseMessageAsync<T>(HttpResponseMessage mes
#if NET451
T entity = await message.Content.ReadAsAsync<T>(token).ConfigureAwait(false);
#else
string str = await message.Content.ReadAsStringAsync().ConfigureAwait(false);
string str = await message.Content.ReadHttpContentAsStringAsync(token).ConfigureAwait(false);
T entity = JsonConvert.DeserializeObject<T>(str);
#endif

// Etag in the header is considered authoritative
var eTagHolder = entity as IETagHolder;
if (eTagHolder != null)
Expand Down
4 changes: 0 additions & 4 deletions e2e/test/E2EMsTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down
4 changes: 2 additions & 2 deletions e2e/test/E2ETests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;netcoreapp2.1.18;net472;net451</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp3.1;netcoreapp2.1.18</TargetFrameworks>
<TargetFrameworks>net5.0;netcoreapp3.1;netcoreapp2.1.18;net472;net451</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">net5.0;netcoreapp3.1;netcoreapp2.1.18</TargetFrameworks>
<LangVersion>8.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion e2e/test/config/TestConfiguration.IoTHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private static string GenerateSasToken(string resourceUri, string sharedAccessKe

string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry;

HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(sharedAccessKey));
using var hmac = new HMACSHA256(Convert.FromBase64String(sharedAccessKey));
string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));

// SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=<Signature>&se=<ExpiresOnValue>[&skn=<KeyName>]
Expand Down
2 changes: 1 addition & 1 deletion e2e/test/helpers/CustomWebProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Devices.E2ETests.Helpers
public class CustomWebProxy : IWebProxy
{
private readonly MsTestLogger _logger;
private long _counter = 0;
private long _counter;

public CustomWebProxy(MsTestLogger logger)
{
Expand Down
2 changes: 1 addition & 1 deletion e2e/test/helpers/StorageContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.Azure.Devices.E2ETests.Helpers
/// </summary>
public class StorageContainer : IDisposable
{
private bool _disposed = false;
private bool _disposed;

public string ContainerName { get; }
public Uri Uri { get; private set; }
Expand Down
Loading

0 comments on commit 4b6752e

Please sign in to comment.