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

Update the logic for ETags and preconditions #13046

Merged
merged 11 commits into from
Jun 29, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ public FilesClient() { }
}
public enum IfMatchPrecondition
{
Unconditional = 0,
UnconditionalIfMatch = 1,
IfMatch = 2,
UnconditionalIfMatch = 0,
IfMatch = 1,
}
public partial class IoTHubServiceClient
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public async Task<ModuleIdentity> UpdateModuleIdentityAsync(string deviceId, str
Console.WriteLine($"Updating module identity with Id: '{moduleIdentity.ModuleId}'. Setting 'ManagedBy' property to: '{Environment.UserName}'");
moduleIdentity.ManagedBy = Environment.UserName;

Response<ModuleIdentity> response = await IoTHubServiceClient.Modules.CreateOrUpdateIdentityAsync(moduleIdentity, IfMatchPrecondition.UnconditionalIfMatch);
Response<ModuleIdentity> response = await IoTHubServiceClient.Modules.CreateOrUpdateIdentityAsync(moduleIdentity);

ModuleIdentity updatedModule = response.Value;

Expand Down Expand Up @@ -256,7 +256,7 @@ public async Task<TwinData> UpdateModuleTwinAsync(string deviceId, string module

moduleTwin.Properties.Desired.Add(new KeyValuePair<string, object>(userPropName, Environment.UserName));

Response<TwinData> response = await IoTHubServiceClient.Modules.UpdateTwinAsync(moduleTwin, IfMatchPrecondition.UnconditionalIfMatch);
Response<TwinData> response = await IoTHubServiceClient.Modules.UpdateTwinAsync(moduleTwin);

TwinData updatedTwin = response.Value;

Expand Down Expand Up @@ -294,7 +294,7 @@ public async Task DeleteModuleIdentityAsync(string deviceId, string moduleId)
Console.WriteLine($"Deleting module identity: DeviceId: '{moduleIdentity.DeviceId}', ModuleId: '{moduleIdentity.ModuleId}', ETag: '{moduleIdentity.Etag}'");

// We use UnconditionalIfMatch to force delete the Module Identity (disregard the IfMatch ETag).
Response response = await IoTHubServiceClient.Modules.DeleteIdentityAsync(moduleIdentity, IfMatchPrecondition.UnconditionalIfMatch);
Response response = await IoTHubServiceClient.Modules.DeleteIdentityAsync(moduleIdentity);

SampleLogger.PrintSuccess($"Successfully deleted module identity: DeviceId: '{deviceId}', ModuleId: '{moduleId}'");
}
Expand All @@ -320,8 +320,7 @@ public async Task DeleteDeviceIdentityAsync(string deviceId)

Console.WriteLine($"Deleting device identity with Id: '{deviceIdentity.DeviceId}'");

// We use UnconditionalIfMatch to force delete the Device Identity (disregard the IfMatch ETag).
Response response = await IoTHubServiceClient.Devices.DeleteIdentityAsync(deviceIdentity, IfMatchPrecondition.UnconditionalIfMatch);
Response response = await IoTHubServiceClient.Devices.DeleteIdentityAsync(deviceIdentity);

SampleLogger.PrintSuccess($"Successfully deleted device identity with Id: '{deviceIdentity.DeviceId}'");
}
Expand Down
18 changes: 12 additions & 6 deletions sdk/iot/Azure.Iot.Hub.Service/src/DevicesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ internal DevicesClient(RegistryManagerRestClient registryManagerClient, TwinRest
/// <summary>
/// Create or update a device identity.
/// </summary>
/// <param name="deviceIdentity">the device identity to create.</param>
/// <param name="precondition">The condition on which to perform this operation. To create a device identity, this value must be equal to <see cref="IfMatchPrecondition.Unconditional"/>.</param>
/// <param name="deviceIdentity">the device identity to create or update.</param>
/// <param name="precondition">The condition on which to perform this operation.
/// In case of create, the condition must be equal to <see cref="IfMatchPrecondition.IfMatch"/>.
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
/// In case of update, if no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.
/// </param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created device identity and the http response <see cref="Response{T}"/>.</returns>
public virtual Task<Response<DeviceIdentity>> CreateOrUpdateIdentityAsync(
Expand All @@ -60,8 +63,11 @@ public virtual Task<Response<DeviceIdentity>> CreateOrUpdateIdentityAsync(
/// <summary>
/// Create or update a device identity.
/// </summary>
/// <param name="deviceIdentity">the device identity to create.</param>
/// <param name="precondition">The condition on which to perform this operation. To create a device identity, this value must be equal to <see cref="IfMatchPrecondition.Unconditional"/>.</param>
/// <param name="deviceIdentity">the device identity to create or update.</param>
/// <param name="precondition">The condition on which to perform this operation.
/// In case of create, the condition must be equal to <see cref="IfMatchPrecondition.IfMatch"/>.
/// In case of update, if no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.
/// </param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created device identity and the http response <see cref="Response{T}"/>.</returns>
public virtual Response<DeviceIdentity> CreateOrUpdateIdentity(
Expand Down Expand Up @@ -99,7 +105,7 @@ public virtual Response<DeviceIdentity> GetIdentity(string deviceId, Cancellatio
/// <summary>
/// Delete a single device identity.
/// </summary>
/// <param name="deviceIdentity">the device identity to delete. If no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.Unconditional"/> or equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="deviceIdentity">the device identity to delete. If no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>."/>.</param>
/// <param name="precondition">The condition on which to delete the device.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The http response <see cref="Response{T}"/>.</returns>
Expand All @@ -116,7 +122,7 @@ public virtual Task<Response> DeleteIdentityAsync(
/// <summary>
/// Delete a single device identity.
/// </summary>
/// <param name="deviceIdentity">the device identity to delete. If no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.Unconditional"/> or equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="deviceIdentity">the device identity to delete. If no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/> or equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="precondition">The condition on which to delete the device.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The http response <see cref="Response{T}"/>.</returns>
Expand Down
6 changes: 0 additions & 6 deletions sdk/iot/Azure.Iot.Hub.Service/src/IfMatchPrecondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ namespace Azure.Iot.Hub.Service
/// </summary>
public enum IfMatchPrecondition
{
/// <summary>
/// Perform this operation regardless of if the provided resource matches the service's representation
/// of the object. This will cause the HTTP request to be sent with no ifMatch header. The service will never respond with a 412 error code with this setting.
/// </summary>
Unconditional,
azabbasi marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Perform this operation as long as the provided resource exists in the service. This will cause the HTTP request to be sent with an ifMatch header with value "*". For create or update
/// operations, if the resource does not exist, then the service will not execute the operation and will respond to the request with a 412 error code. For delete operations, if the resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,12 @@ internal static class IfMatchPreconditionExtensions
/// <returns>The ifMatch header value.</returns>
internal static string GetIfMatchHeaderValue(IfMatchPrecondition precondition, string ETag)
{
if (precondition == IfMatchPrecondition.IfMatch)
return precondition switch
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
{
return ETag;
}
else if (precondition == IfMatchPrecondition.UnconditionalIfMatch)
{
return "*";
}
else //precondition == IfMatchPrecondition.Unconditional
{
return null;
}
IfMatchPrecondition.IfMatch => $"\"{ETag}\"",
IfMatchPrecondition.UnconditionalIfMatch => "*",
_ => null,
};
}
}
}
18 changes: 12 additions & 6 deletions sdk/iot/Azure.Iot.Hub.Service/src/ModulesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ internal ModulesClient(RegistryManagerRestClient registryManagerClient, TwinRest
/// <summary>
/// Create a module identity.
/// </summary>
/// <param name="moduleIdentity">The module identity to create.</param>
/// <param name="precondition">The condition on which to perform this operation. To create a module identity, this value must be equal to <see cref="IfMatchPrecondition.Unconditional"/>.</param>
/// <param name="moduleIdentity">The module identity to create or update.</param>
/// <param name="precondition">The condition on which to perform this operation.
/// In case of create, the condition must be equal to <see cref="IfMatchPrecondition.IfMatch"/>.
/// In case of update, if no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.
/// </param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created module identity and the http response <see cref="Response{T}"/>.</returns>
public virtual Task<Response<ModuleIdentity>> CreateOrUpdateIdentityAsync(
Expand All @@ -58,8 +61,11 @@ public virtual Task<Response<ModuleIdentity>> CreateOrUpdateIdentityAsync(
/// <summary>
/// Create a module identity.
/// </summary>
/// <param name="moduleIdentity">The module identity to create.</param>
/// <param name="precondition">The condition on which to perform this operation. To create a module identity, this value must be equal to <see cref="IfMatchPrecondition.Unconditional"/>.</param>
/// <param name="moduleIdentity">The module identity to create or update.</param>
/// <param name="precondition">The condition on which to perform this operation.
/// In case of create, the condition must be equal to <see cref="IfMatchPrecondition.IfMatch"/>.
/// In case of update, if no ETag is present on the device, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.
/// </param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created module identity and the http response <see cref="Response{T}"/>.</returns>
public virtual Response<ModuleIdentity> CreateOrUpdateIdentity(
Expand Down Expand Up @@ -123,7 +129,7 @@ public virtual Response<IReadOnlyList<ModuleIdentity>> GetIdentities(string devi
/// <summary>
/// Delete a single module identity.
/// </summary>
/// <param name="moduleIdentity">The module identity to delete. If no ETag is present on the module identity, then the condition must be equal to <see cref="IfMatchPrecondition.Unconditional"/> or equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="moduleIdentity">The module identity to delete. If no ETag is present on the module identity, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="precondition">The condition on which to delete the module identity.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The http response <see cref="Response{T}"/>.</returns>
Expand All @@ -140,7 +146,7 @@ public virtual Task<Response> DeleteIdentityAsync(
/// <summary>
/// Delete a single module identity.
/// </summary>
/// <param name="moduleIdentity">The module identity to delete. If no ETag is present on the module identity, then the condition must be equal to <see cref="IfMatchPrecondition.Unconditional"/> or equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="moduleIdentity">The module identity to delete. If no ETag is present on the module identity, then the condition must be equal to <see cref="IfMatchPrecondition.UnconditionalIfMatch"/>.</param>
/// <param name="precondition">The condition on which to delete the module identity.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The http response <see cref="Response{T}"/>.</returns>
Expand Down
52 changes: 52 additions & 0 deletions sdk/iot/Azure.Iot.Hub.Service/tests/DevicesClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ public async Task DevicesClient_IdentityLifecycle()
}
}

/// <summary>
/// Test the logic for ETag if-match header
/// </summary>
[Test]
public async Task DevicesClient_UpdateDevice_EtagDoesNotMatch()
{
string testDeviceId = $"UpdateWithETag{GetRandom()}";

DeviceIdentity device = null;
IoTHubServiceClient client = GetClient();

try
{
// Create a device
Response<DeviceIdentity> createResponse = await client.Devices.CreateOrUpdateIdentityAsync(
new Models.DeviceIdentity
{
DeviceId = testDeviceId
}).ConfigureAwait(false);

// Store the device object to later update it with invalid ETag
device = createResponse.Value;

// Update the device to get a new ETag value.
device.Status = DeviceStatus.Disabled;
Response<DeviceIdentity> getResponse = await client.Devices.CreateOrUpdateIdentityAsync(device).ConfigureAwait(false);
DeviceIdentity updatedDevice = getResponse.Value;

Assert.AreNotEqual(updatedDevice.Etag, device.Etag, "ETag should have been updated.");

// Perform another update using the old device object to verify precondition fails.
device.Status = DeviceStatus.Enabled;
try
{
Response<DeviceIdentity> updateResponse = await client.Devices.CreateOrUpdateIdentityAsync(device).ConfigureAwait(false);
Assert.Fail($"Update call with outdated ETag should fail with 412 (PreconditionFailed)");
}
// We will catch the exception and verify status is 412 (PreconditionfFailed)
catch (RequestFailedException ex)
{
Assert.AreEqual(412, ex.Status, $"Expected the update to fail with http status code 412 (PreconditionFailed)");
}

// Perform the same update and ignore the ETag value by providing UnconditionalIfMatch precondition
await client.Devices.CreateOrUpdateIdentityAsync(device, IfMatchPrecondition.UnconditionalIfMatch).ConfigureAwait(false);
}
finally
{
await Cleanup(client, device);
}
}

/// <summary>
/// Test basic operations of a Device Twin.
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading