Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the Azure.Core ETag type in the DPS service client #2864

Merged
merged 6 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SDK v2 migration guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ to migrate to version 2.x when they have the chance. For more details on LTS rel
#### Other notable breaking changes

- Query methods (like for individual and group enrollments) now take a query string (and optionally a page size parameter), and the `Query` result no longer requires disposing.
- ETag fields on the classes `IndividualEnrollment`, `EnrollmentGroup`, and `DeviceRegistrationState` are now taken as the `Azure.ETag` type instead of strings.

### Authentication provider client

Expand Down
14 changes: 1 addition & 13 deletions iothub/device/src/Transport/Http/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,7 @@ private static async Task<T> ReadResponseMessageAsync<T>(HttpResponseMessage mes
return (T)(object)message;
}

T entity = await ReadAsAsync<T>(message.Content, token).ConfigureAwait(false);

// ETag in the header is considered authoritative
if (entity is IETagHolder eTagHolder)
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
{
if (message.Headers.ETag != null && !string.IsNullOrWhiteSpace(message.Headers.ETag.Tag))
{
// RDBug 3429280:Make the version field of Device object internal
eTagHolder.ETag = message.Headers.ETag.Tag;
}
}

return entity;
return await ReadAsAsync<T>(message.Content, token).ConfigureAwait(false);
}

private static void AddCustomHeaders(HttpRequestMessage requestMessage, IDictionary<string, string> customHeaders)
Expand Down
16 changes: 0 additions & 16 deletions iothub/device/src/Transport/Http/IETagHolder.cs

This file was deleted.

3 changes: 2 additions & 1 deletion provisioning/service/src/DeviceRegistrationsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using System.Threading.Tasks;
using System.Threading;
using Azure;
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Service
Expand Down Expand Up @@ -60,7 +61,7 @@ public async Task<DeviceRegistrationState> GetAsync(string registrationId, Cance
GetDeviceRegistrationStatusUri(registrationId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down
9 changes: 5 additions & 4 deletions provisioning/service/src/EnrollmentGroupsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net;
using System.Threading.Tasks;
using System.Threading;
using Azure;
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Service
Expand Down Expand Up @@ -101,7 +102,7 @@ public async Task<EnrollmentGroup> GetAsync(string enrollmentGroupId, Cancellati
GetEnrollmentUri(enrollmentGroupId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -131,7 +132,7 @@ await _contractApiHttp
GetEnrollmentUri(enrollmentGroupId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);
}
Expand Down Expand Up @@ -201,7 +202,7 @@ public async Task<BulkEnrollmentOperationResult> RunBulkOperationAsync(
GetEnrollmentUri(),
null,
JsonConvert.SerializeObject(bulkOperation),
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -257,7 +258,7 @@ public async Task<AttestationMechanism> GetAttestationAsync(string enrollmentGro
GetEnrollmentAttestationUri(enrollmentGroupId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down
35 changes: 19 additions & 16 deletions provisioning/service/src/Http/ContractApiHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Service
Expand Down Expand Up @@ -95,7 +96,7 @@ public ContractApiHttp(
/// <param name="requestUri">the rest API <see cref="Uri"/> with for the requested service.</param>
/// <param name="customHeaders">the optional Dictionary with additional header fields. It can be null.</param>
/// <param name="body">the string with the message body. It can be null or empty.</param>
/// <param name="ifMatch">the optional string with the match condition, normally an eTag. It can be null.</param>
/// <param name="eTag">the optional string with the match condition, normally an eTag. It can be null.</param>
/// <param name="cancellationToken">the task cancellation Token.</param>
/// <returns>The <see cref="ContractApiResponse"/> with the HTTP response.</returns>
/// <exception cref="OperationCanceledException">If the cancellation was requested.</exception>
Expand All @@ -106,7 +107,7 @@ public async Task<ContractApiResponse> RequestAsync(
Uri requestUri,
IDictionary<string, string> customHeaders,
string body,
string ifMatch,
ETag eTag,
CancellationToken cancellationToken)
{
ContractApiResponse response;
Expand All @@ -127,7 +128,12 @@ public async Task<ContractApiResponse> RequestAsync(
msg.Headers.Add(header.Key, header.Value);
}
}
InsertIfMatch(msg, ifMatch);

if (!string.IsNullOrWhiteSpace(eTag.ToString()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can ETag be null?

Copy link
Member Author

@timtay-microsoft timtay-microsoft Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. It's a struct that may have a null string inside it, though.

{
string escapedETag = EscapeETag(eTag.ToString());
msg.Headers.IfMatch.Add(new EntityTagHeaderValue(escapedETag));
}

try
{
Expand Down Expand Up @@ -217,28 +223,25 @@ private static void ValidateHttpResponse(ContractApiResponse response)
}
}

private static void InsertIfMatch(HttpRequestMessage requestMessage, string ifMatch)
// ETag values other than "*" need to be wrapped in escaped quotes if they are not
// already.
private static string EscapeETag(string eTag)
{
if (string.IsNullOrWhiteSpace(ifMatch))
{
return;
}

var quotedIfMatch = new StringBuilder();
var escapedETagBuilder = new StringBuilder();

if (!ifMatch.StartsWith("\"", StringComparison.OrdinalIgnoreCase))
if (!eTag.StartsWith("\"", StringComparison.OrdinalIgnoreCase))
{
quotedIfMatch.Append('"');
escapedETagBuilder.Append('"');
}

quotedIfMatch.Append(ifMatch);
escapedETagBuilder.Append(eTag);

if (!ifMatch.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
if (!eTag.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
{
quotedIfMatch.Append('"');
escapedETagBuilder.Append('"');
}

requestMessage.Headers.IfMatch.Add(new EntityTagHeaderValue(quotedIfMatch.ToString()));
return escapedETagBuilder.ToString();
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion provisioning/service/src/Http/IContractApiHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Azure;

namespace Microsoft.Azure.Devices.Provisioning.Service
{
Expand All @@ -16,7 +17,7 @@ Task<ContractApiResponse> RequestAsync(
Uri requestUri,
IDictionary<string, string> customHeaders,
string body,
string ifMatch,
ETag ifMatch,
CancellationToken cancellationToken);
}
}
9 changes: 5 additions & 4 deletions provisioning/service/src/IndividualEnrollmentsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using System.Threading;
using Newtonsoft.Json;
using Azure;
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved

namespace Microsoft.Azure.Devices.Provisioning.Service
{
Expand Down Expand Up @@ -91,7 +92,7 @@ public async Task<IndividualEnrollment> GetAsync(string registrationId, Cancella
GetEnrollmentUri(registrationId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -121,7 +122,7 @@ await _contractApiHttp
GetEnrollmentUri(registrationId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);
}
Expand Down Expand Up @@ -191,7 +192,7 @@ public async Task<BulkEnrollmentOperationResult> RunBulkOperationAsync(
GetEnrollmentUri(),
null,
JsonConvert.SerializeObject(bulkOperation),
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -248,7 +249,7 @@ public async Task<AttestationMechanism> GetAttestationAsync(string registrationI
GetEnrollmentAttestationUri(registrationId),
null,
null,
null,
new ETag(),
cancellationToken)
.ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

<!--Nuget package dependencies-->
<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.25.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Azure;
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Service
Expand Down Expand Up @@ -72,6 +73,7 @@ public DeviceRegistrationState(string registrationId)
/// Registration status ETag.
/// </summary>
[JsonProperty(PropertyName = "etag", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ETag { get; internal set; }
[JsonConverter(typeof(NewtonsoftJsonETagConverter))] // NewtonsoftJsonETagConverter is used here because otherwise the ETag isn't serialized properly
public ETag ETag { get; internal set; }
}
}
86 changes: 18 additions & 68 deletions provisioning/service/src/Models/EnrollmentGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Linq;
using System.Net;
using Azure;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

Expand Down Expand Up @@ -80,7 +81,7 @@ namespace Microsoft.Azure.Devices.Provisioning.Service
/// }
/// </code>
/// </example>
public class EnrollmentGroup : IETagHolder
public class EnrollmentGroup
{
/// <summary>
/// Creates a new instance of EnrollmentGroup.
Expand Down Expand Up @@ -121,54 +122,9 @@ public EnrollmentGroup(string enrollmentGroupId, Attestation attestation)
Attestation = attestation;
}

/// <summary>
/// Creates a new instance of EnrollmentGroup using information in a JSON.
/// </summary>
/// <remarks>
/// This constructor creates an instance of the enrollmentGroup filling the class with the information
/// provided in the JSON. It is used by the SDK to parse EnrollmentGroup responses from the provisioning service.
/// </remarks>
/// <example>
/// The following JSON is a sample of the EnrollmentGroup response, received from the provisioning service.
/// <code>
/// {
/// "enrollmentGroupId":"validEnrollmentGroupId",
/// "attestation":{
/// "type":"x509",
/// "signingCertificates":{
/// "primary":{
/// "certificate":"[valid certificate]",
/// "info": {
/// "subjectName": "CN=ROOT_00000000-0000-0000-0000-000000000000, OU=Azure IoT, O=MSFT, C=US",
/// "sha1Thumbprint": "0000000000000000000000000000000000",
/// "sha256Thumbprint": "validEnrollmentGroupId",
/// "issuerName": "CN=ROOT_00000000-0000-0000-0000-000000000000, OU=Azure IoT, O=MSFT, C=US",
/// "notBeforeUtc": "2017-11-14T12:34:18Z",
/// "notAfterUtc": "2017-11-20T12:34:18Z",
/// "serialNumber": "000000000000000000",
/// "version": 3
/// }
/// }
/// }
/// },
/// "iotHubHostName":"ContosoIoTHub.azure-devices.net",
/// "provisioningStatus":"enabled",
/// "createdDateTimeUtc": "2017-09-28T16:29:42.3447817Z",
/// "lastUpdatedDateTimeUtc": "2017-09-28T16:29:42.3447817Z",
/// "etag": "\"00000000-0000-0000-0000-00000000000\""
/// }
/// </code>
/// </example>
/// <param name="enrollmentGroupId">The string with a unique id for the enrollmentGroup. It cannot be null or empty.</param>
/// <param name="attestation">The <see cref="AttestationMechanism"/> for the enrollment. It shall be `X509` or `SymmetricKey`.</param>
/// <param name="iotHubHostName">The string with the target IoT hub name. This is optional and can be null or empty.</param>
/// <param name="initialTwinState">The <see cref="TwinState"/> with the initial Twin condition. This is optional and can be null.</param>
/// <param name="provisioningStatus">The <see cref="ProvisioningStatus"/> that determine the initial status of the device. This is optional and can be null.</param>
/// <param name="createdDateTimeUtc">The DateTime with the date and time that the enrollment was created. This is optional and can be null.</param>
/// <param name="lastUpdatedDateTimeUtc">The DateTime with the date and time that the enrollment was updated. This is optional and can be null.</param>
/// <param name="eTag">The string with the eTag that identify the correct instance of the enrollment in the service. It cannot be null or empty.</param>
/// <param name="capabilities">The capabilities of the device (ie: is it an edge device?)</param>
/// <exception cref="DeviceProvisioningServiceException">If the received JSON is invalid.</exception>
// This JsonConstructor is used for serialization instead of the usual empty constructor
// because one of this object's fields (attestation) doesn't map 1:1 with where that field
// is in the JSON the service sends.
[JsonConstructor]
internal EnrollmentGroup(
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
string enrollmentGroupId,
Expand All @@ -178,30 +134,23 @@ internal EnrollmentGroup(
ProvisioningStatus? provisioningStatus,
DateTime createdDateTimeUtc,
DateTime lastUpdatedDateTimeUtc,
string eTag,
ETag eTag,
DeviceCapabilities capabilities)
{
if (attestation == null)
{
throw new DeviceProvisioningServiceException("Service responds an enrollmentGroup without attestation.", HttpStatusCode.BadRequest);
throw new DeviceProvisioningServiceException("Service responded with an enrollment without attestation.", HttpStatusCode.BadRequest);
}

try
{
EnrollmentGroupId = enrollmentGroupId;
Attestation = attestation.GetAttestation();
IotHubHostName = iotHubHostName;
InitialTwinState = initialTwinState;
ProvisioningStatus = provisioningStatus;
CreatedDateTimeUtc = createdDateTimeUtc;
LastUpdatedDateTimeUtc = lastUpdatedDateTimeUtc;
ETag = eTag;
Capabilities = capabilities;
}
catch (ArgumentException e)
{
throw new DeviceProvisioningServiceException(e.Message, HttpStatusCode.BadRequest, e);
}
EnrollmentGroupId = enrollmentGroupId;
Attestation = attestation.GetAttestation(); // This is the one reason why we can't use an empty constructor here.
IotHubHostName = iotHubHostName;
InitialTwinState = initialTwinState;
ProvisioningStatus = provisioningStatus;
CreatedDateTimeUtc = createdDateTimeUtc;
LastUpdatedDateTimeUtc = lastUpdatedDateTimeUtc;
ETag = eTag;
Capabilities = capabilities;
}

/// <summary>
Expand Down Expand Up @@ -304,7 +253,8 @@ public Attestation Attestation
/// Enrollment's ETag.
/// </summary>
[JsonProperty(PropertyName = "etag", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ETag { get; set; }
[JsonConverter(typeof(NewtonsoftJsonETagConverter))] // NewtonsoftJsonETagConverter is used here because otherwise the ETag isn't serialized properly
public ETag ETag { get; set; }

/// <summary>
/// Capabilities of the device.
Expand Down
Loading