From 672efe2630ae913f7a49fafbd7309d804c44ecc8 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:29:23 -0600 Subject: [PATCH 01/26] Initial CodeLoom project and file creation. --- dotnetv4/IoT/Actions/HelloIoT.cs | 75 ++++ dotnetv4/IoT/Actions/IoTActions.csproj | 24 ++ dotnetv4/IoT/Actions/IoTWrapper.cs | 471 ++++++++++++++++++++++ dotnetv4/IoT/IoTExamples.sln | 36 ++ dotnetv4/IoT/Scenarios/IoTBasics.cs | 355 ++++++++++++++++ dotnetv4/IoT/Scenarios/IoTBasics.csproj | 28 ++ dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 163 ++++++++ dotnetv4/IoT/Tests/IoTTests.csproj | 34 ++ 8 files changed, 1186 insertions(+) create mode 100644 dotnetv4/IoT/Actions/HelloIoT.cs create mode 100644 dotnetv4/IoT/Actions/IoTActions.csproj create mode 100644 dotnetv4/IoT/Actions/IoTWrapper.cs create mode 100644 dotnetv4/IoT/IoTExamples.sln create mode 100644 dotnetv4/IoT/Scenarios/IoTBasics.cs create mode 100644 dotnetv4/IoT/Scenarios/IoTBasics.csproj create mode 100644 dotnetv4/IoT/Tests/IoTIntegrationTests.cs create mode 100644 dotnetv4/IoT/Tests/IoTTests.csproj diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs new file mode 100644 index 00000000000..60a61f9671f --- /dev/null +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.Hello] +/// +/// Hello AWS IoT example. +/// +public class HelloIoT +{ + /// + /// Main method to run the Hello IoT example. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + var iotClient = new AmazonIoTClient(); + + try + { + Console.WriteLine("Hello AWS IoT! Let's list your IoT Things:"); + Console.WriteLine(new string('-', 80)); + + var request = new ListThingsRequest + { + MaxResults = 10 + }; + + var response = await iotClient.ListThingsAsync(request); + + if (response.Things.Count > 0) + { + Console.WriteLine($"Found {response.Things.Count} IoT Things:"); + foreach (var thing in response.Things) + { + Console.WriteLine($"- Thing Name: {thing.ThingName}"); + Console.WriteLine($" Thing ARN: {thing.ThingArn}"); + Console.WriteLine($" Thing Type: {thing.ThingTypeName ?? "No type specified"}"); + Console.WriteLine($" Version: {thing.Version}"); + + if (thing.Attributes?.Count > 0) + { + Console.WriteLine(" Attributes:"); + foreach (var attr in thing.Attributes) + { + Console.WriteLine($" {attr.Key}: {attr.Value}"); + } + } + Console.WriteLine(); + } + } + else + { + Console.WriteLine("No IoT Things found in your account."); + Console.WriteLine("You can create IoT Things using the IoT Basics scenario example."); + } + + Console.WriteLine("Hello IoT completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + finally + { + iotClient.Dispose(); + } + } +} +// snippet-end:[iot.dotnetv4.Hello] diff --git a/dotnetv4/IoT/Actions/IoTActions.csproj b/dotnetv4/IoT/Actions/IoTActions.csproj new file mode 100644 index 00000000000..8a0ab2e6143 --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTActions.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs new file mode 100644 index 00000000000..e070dd27233 --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -0,0 +1,471 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; +using Amazon.IotData; +using Amazon.IotData.Model; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.IoTWrapper] +/// +/// Wrapper methods to use Amazon IoT Core with .NET. +/// +public class IoTWrapper +{ + private readonly IAmazonIoT _amazonIoT; + private readonly IAmazonIotData _amazonIotData; + private readonly ILogger _logger; + + /// + /// Constructor for the IoT wrapper. + /// + /// The injected IoT client. + /// The injected IoT Data client. + /// The injected logger. + public IoTWrapper(IAmazonIoT amazonIoT, IAmazonIotData amazonIotData, ILogger logger) + { + _amazonIoT = amazonIoT; + _amazonIotData = amazonIotData; + _logger = logger; + } + + // snippet-start:[iot.dotnetv4.CreateThing] + /// + /// Creates an AWS IoT Thing. + /// + /// The name of the Thing to create. + /// The ARN of the Thing created. + public async Task CreateThingAsync(string thingName) + { + try + { + var request = new CreateThingRequest + { + ThingName = thingName + }; + + var response = await _amazonIoT.CreateThingAsync(request); + _logger.LogInformation($"Created Thing {thingName} with ARN {response.ThingArn}"); + return response.ThingArn; + } + catch (Exception ex) + { + _logger.LogError($"Error creating Thing {thingName}: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateThing] + + // snippet-start:[iot.dotnetv4.CreateKeysAndCertificate] + /// + /// Creates a device certificate for AWS IoT. + /// + /// The certificate details including ARN and certificate PEM. + public async Task<(string CertificateArn, string CertificatePem, string CertificateId)> CreateKeysAndCertificateAsync() + { + try + { + var request = new CreateKeysAndCertificateRequest + { + SetAsActive = true + }; + + var response = await _amazonIoT.CreateKeysAndCertificateAsync(request); + _logger.LogInformation($"Created certificate with ARN {response.CertificateArn}"); + return (response.CertificateArn, response.CertificatePem, response.CertificateId); + } + catch (Exception ex) + { + _logger.LogError($"Error creating certificate: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateKeysAndCertificate] + + // snippet-start:[iot.dotnetv4.AttachThingPrincipal] + /// + /// Attaches a certificate to an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to attach. + /// True if successful. + public async Task AttachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new AttachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.AttachThingPrincipalAsync(request); + _logger.LogInformation($"Attached certificate {certificateArn} to Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error attaching certificate to Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.AttachThingPrincipal] + + // snippet-start:[iot.dotnetv4.UpdateThing] + /// + /// Updates an IoT Thing with attributes. + /// + /// The name of the Thing to update. + /// Dictionary of attributes to add. + /// True if successful. + public async Task UpdateThingAsync(string thingName, Dictionary attributes) + { + try + { + var request = new UpdateThingRequest + { + ThingName = thingName, + AttributePayload = new AttributePayload + { + Attributes = attributes, + Merge = true + } + }; + + await _amazonIoT.UpdateThingAsync(request); + _logger.LogInformation($"Updated Thing {thingName} with attributes"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error updating Thing attributes: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.UpdateThing] + + // snippet-start:[iot.dotnetv4.DescribeEndpoint] + /// + /// Gets the AWS IoT endpoint URL. + /// + /// The endpoint URL. + public async Task DescribeEndpointAsync() + { + try + { + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + + var response = await _amazonIoT.DescribeEndpointAsync(request); + _logger.LogInformation($"Retrieved endpoint: {response.EndpointAddress}"); + return response.EndpointAddress; + } + catch (Exception ex) + { + _logger.LogError($"Error describing endpoint: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DescribeEndpoint] + + // snippet-start:[iot.dotnetv4.ListCertificates] + /// + /// Lists all certificates associated with the account. + /// + /// List of certificate information. + public async Task> ListCertificatesAsync() + { + try + { + var request = new ListCertificatesRequest(); + var response = await _amazonIoT.ListCertificatesAsync(request); + + _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); + return response.Certificates; + } + catch (Exception ex) + { + _logger.LogError($"Error listing certificates: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListCertificates] + + // snippet-start:[iot.dotnetv4.UpdateThingShadow] + /// + /// Updates the Thing's shadow with new state information. + /// + /// The name of the Thing. + /// The shadow payload in JSON format. + /// True if successful. + public async Task UpdateThingShadowAsync(string thingName, string shadowPayload) + { + try + { + var request = new UpdateThingShadowRequest + { + ThingName = thingName, + Payload = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(shadowPayload)) + }; + + await _amazonIotData.UpdateThingShadowAsync(request); + _logger.LogInformation($"Updated shadow for Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error updating Thing shadow: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.UpdateThingShadow] + + // snippet-start:[iot.dotnetv4.GetThingShadow] + /// + /// Gets the Thing's shadow information. + /// + /// The name of the Thing. + /// The shadow data as a string. + public async Task GetThingShadowAsync(string thingName) + { + try + { + var request = new GetThingShadowRequest + { + ThingName = thingName + }; + + var response = await _amazonIotData.GetThingShadowAsync(request); + using var reader = new StreamReader(response.Payload); + var shadowData = await reader.ReadToEndAsync(); + + _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); + return shadowData; + } + catch (Exception ex) + { + _logger.LogError($"Error getting Thing shadow: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.GetThingShadow] + + // snippet-start:[iot.dotnetv4.CreateTopicRule] + /// + /// Creates an IoT topic rule. + /// + /// The name of the rule. + /// The ARN of the SNS topic for the action. + /// The ARN of the IAM role. + /// True if successful. + public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn, string roleArn) + { + try + { + var request = new CreateTopicRuleRequest + { + RuleName = ruleName, + TopicRulePayload = new TopicRulePayload + { + Sql = "SELECT * FROM 'topic/subtopic'", + Description = $"Rule created by .NET example: {ruleName}", + Actions = new List + { + new Amazon.IoT.Model.Action + { + Sns = new SnsAction + { + TargetArn = snsTopicArn, + RoleArn = roleArn + } + } + }, + RuleDisabled = false + } + }; + + await _amazonIoT.CreateTopicRuleAsync(request); + _logger.LogInformation($"Created IoT rule {ruleName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error creating topic rule: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateTopicRule] + + // snippet-start:[iot.dotnetv4.ListTopicRules] + /// + /// Lists all IoT topic rules. + /// + /// List of topic rules. + public async Task> ListTopicRulesAsync() + { + try + { + var request = new ListTopicRulesRequest(); + var response = await _amazonIoT.ListTopicRulesAsync(request); + + _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); + return response.Rules; + } + catch (Exception ex) + { + _logger.LogError($"Error listing topic rules: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListTopicRules] + + // snippet-start:[iot.dotnetv4.SearchIndex] + /// + /// Searches for IoT Things using the search index. + /// + /// The search query string. + /// List of Things that match the search criteria. + public async Task> SearchIndexAsync(string queryString) + { + try + { + var request = new SearchIndexRequest + { + IndexName = "AWS_Things", + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things"); + return response.Things; + } + catch (Exception ex) + { + _logger.LogError($"Error searching index: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.SearchIndex] + + // snippet-start:[iot.dotnetv4.DetachThingPrincipal] + /// + /// Detaches a certificate from an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to detach. + /// True if successful. + public async Task DetachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new DetachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.DetachThingPrincipalAsync(request); + _logger.LogInformation($"Detached certificate {certificateArn} from Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error detaching certificate from Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DetachThingPrincipal] + + // snippet-start:[iot.dotnetv4.DeleteCertificate] + /// + /// Deletes an IoT certificate. + /// + /// The ID of the certificate to delete. + /// True if successful. + public async Task DeleteCertificateAsync(string certificateId) + { + try + { + // First, update the certificate to inactive state + var updateRequest = new UpdateCertificateRequest + { + CertificateId = certificateId, + NewStatus = CertificateStatus.INACTIVE + }; + await _amazonIoT.UpdateCertificateAsync(updateRequest); + + // Then delete the certificate + var deleteRequest = new DeleteCertificateRequest + { + CertificateId = certificateId + }; + + await _amazonIoT.DeleteCertificateAsync(deleteRequest); + _logger.LogInformation($"Deleted certificate {certificateId}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error deleting certificate: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DeleteCertificate] + + // snippet-start:[iot.dotnetv4.DeleteThing] + /// + /// Deletes an IoT Thing. + /// + /// The name of the Thing to delete. + /// True if successful. + public async Task DeleteThingAsync(string thingName) + { + try + { + var request = new DeleteThingRequest + { + ThingName = thingName + }; + + await _amazonIoT.DeleteThingAsync(request); + _logger.LogInformation($"Deleted Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error deleting Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DeleteThing] + + // snippet-start:[iot.dotnetv4.ListThings] + /// + /// Lists IoT Things with pagination support. + /// + /// List of Things. + public async Task> ListThingsAsync() + { + try + { + var request = new ListThingsRequest(); + var response = await _amazonIoT.ListThingsAsync(request); + + _logger.LogInformation($"Retrieved {response.Things.Count} Things"); + return response.Things; + } + catch (Exception ex) + { + _logger.LogError($"Error listing Things: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListThings] +} +// snippet-end:[iot.dotnetv4.IoTWrapper] diff --git a/dotnetv4/IoT/IoTExamples.sln b/dotnetv4/IoT/IoTExamples.sln new file mode 100644 index 00000000000..812279b9465 --- /dev/null +++ b/dotnetv4/IoT/IoTExamples.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTActions", "Actions\IoTActions.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0001}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTBasics", "Scenarios\IoTBasics.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0002}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTTests", "Tests\IoTTests.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0003}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C73BCD70-F2E4-4A89-9E94-47E8C2B48A41} + EndGlobalSection +EndGlobal diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs new file mode 100644 index 00000000000..da7a2243b95 --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -0,0 +1,355 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IotData; +using Amazon.IoT.Model; +using IoTActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace IoTScenarios; + +// snippet-start:[iot.dotnetv4.IoTScenario] +/// +/// Scenario class for AWS IoT basics workflow. +/// +public class IoTBasics +{ + private static IoTWrapper _iotWrapper = null!; + private static ILogger _logger = null!; + + /// + /// Main method for the IoT Basics scenario. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + // Set up dependency injection for the Amazon service. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddAWSService() + .AddTransient() + .AddLogging(builder => builder.AddConsole()) + ) + .Build(); + + _logger = LoggerFactory.Create(builder => builder.AddConsole()) + .CreateLogger(); + + _iotWrapper = host.Services.GetRequiredService(); + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Welcome to the AWS IoT example workflow."); + Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); + Console.WriteLine("The program guides you through a series of steps, including creating an IoT Thing, generating a device certificate,"); + Console.WriteLine("updating the Thing with attributes, and so on. It utilizes the AWS SDK for .NET and incorporates functionalities"); + Console.WriteLine("for creating and managing IoT Things, certificates, rules, shadows, and performing searches."); + Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); + Console.WriteLine("developers working with AWS IoT in a .NET environment."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + Console.WriteLine(new string('-', 80)); + + try + { + await RunScenarioAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was a problem running the scenario."); + Console.WriteLine($"\nAn error occurred: {ex.Message}"); + } + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("The AWS IoT workflow has successfully completed."); + Console.WriteLine(new string('-', 80)); + } + + /// + /// Run the IoT Basics scenario. + /// + /// A Task object. + private static async Task RunScenarioAsync() + { + string thingName = ""; + string certificateArn = ""; + string certificateId = ""; + string ruleName = ""; + + try + { + // Step 1: Create an AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("1. Create an AWS IoT Thing."); + Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); + Console.WriteLine(); + Console.Write("Enter Thing name: "); + thingName = Console.ReadLine()!; + + var thingArn = await _iotWrapper.CreateThingAsync(thingName); + Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); + Console.WriteLine(new string('-', 80)); + + // Step 2: Generate a Device Certificate + Console.WriteLine(new string('-', 80)); + Console.WriteLine("2. Generate a device certificate."); + Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); + Console.WriteLine(); + Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); + var createCert = Console.ReadLine(); + + if (createCert?.ToLower() == "y") + { + var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + certificateArn = certArn; + certificateId = certId; + + Console.WriteLine($"\nCertificate:"); + // Show only first few lines of certificate for brevity + var lines = certPem.Split('\n'); + for (int i = 0; i < Math.Min(lines.Length, 5); i++) + { + Console.WriteLine(lines[i]); + } + if (lines.Length > 5) + { + Console.WriteLine("..."); + } + + Console.WriteLine($"\nCertificate ARN:"); + Console.WriteLine(certificateArn); + + // Step 3: Attach the Certificate to the AWS IoT Thing + Console.WriteLine("Attach the certificate to the AWS IoT Thing."); + await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + Console.WriteLine("Certificate attached to Thing successfully."); + + Console.WriteLine("Thing Details:"); + Console.WriteLine($"Thing Name: {thingName}"); + Console.WriteLine($"Thing ARN: {thingArn}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 4: Update an AWS IoT Thing with Attributes + Console.WriteLine(new string('-', 80)); + Console.WriteLine("3. Update an AWS IoT Thing with Attributes."); + Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); + Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var attributes = new Dictionary + { + { "Location", "Seattle" }, + { "DeviceType", "Sensor" }, + { "Firmware", "1.2.3" } + }; + + await _iotWrapper.UpdateThingAsync(thingName, attributes); + Console.WriteLine("Thing attributes updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 5: Return a unique endpoint specific to the Amazon Web Services account + Console.WriteLine(new string('-', 80)); + Console.WriteLine("4. Return a unique endpoint specific to the Amazon Web Services account."); + Console.WriteLine("An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var endpoint = await _iotWrapper.DescribeEndpointAsync(); + var subdomain = endpoint.Split('.')[0]; + Console.WriteLine($"Extracted subdomain: {subdomain}"); + Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + Console.WriteLine(new string('-', 80)); + + // Step 6: List your AWS IoT certificates + Console.WriteLine(new string('-', 80)); + Console.WriteLine("5. List your AWS IoT certificates"); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var certificates = await _iotWrapper.ListCertificatesAsync(); + foreach (var cert in certificates.Take(5)) // Show first 5 certificates + { + Console.WriteLine($"Cert id: {cert.CertificateId}"); + Console.WriteLine($"Cert Arn: {cert.CertificateArn}"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 7: Create an IoT shadow + Console.WriteLine(new string('-', 80)); + Console.WriteLine("6. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); + Console.WriteLine("A Thing Shadow refers to a feature that enables you to create a virtual representation, or \"shadow,\""); + Console.WriteLine("of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between"); + Console.WriteLine("the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var shadowPayload = JsonSerializer.Serialize(new + { + state = new + { + desired = new + { + temperature = 25, + humidity = 50 + } + } + }); + + await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + Console.WriteLine("Thing Shadow updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 8: Write out the state information, in JSON format + Console.WriteLine(new string('-', 80)); + Console.WriteLine("7. Write out the state information, in JSON format."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + Console.WriteLine($"Received Shadow Data: {shadowData}"); + Console.WriteLine(new string('-', 80)); + + // Step 9: Creates a rule + Console.WriteLine(new string('-', 80)); + Console.WriteLine("8. Creates a rule"); + Console.WriteLine("Creates a rule that is an administrator-level action."); + Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); + Console.WriteLine(); + Console.Write("Enter Rule name: "); + ruleName = Console.ReadLine()!; + + // Note: For demonstration, we'll use placeholder ARNs + // In real usage, these should be actual SNS topic and IAM role ARNs + var snsTopicArn = "arn:aws:sns:us-east-1:123456789012:example-topic"; + var roleArn = "arn:aws:iam::123456789012:role/IoTRole"; + + Console.WriteLine("Note: Using placeholder ARNs for SNS topic and IAM role."); + Console.WriteLine("In production, ensure these ARNs exist and have proper permissions."); + + try + { + await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + Console.WriteLine("IoT Rule created successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Note: Rule creation failed (expected with placeholder ARNs): {ex.Message}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 10: List your rules + Console.WriteLine(new string('-', 80)); + Console.WriteLine("9. List your rules."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var rules = await _iotWrapper.ListTopicRulesAsync(); + Console.WriteLine("List of IoT Rules:"); + foreach (var rule in rules.Take(5)) // Show first 5 rules + { + Console.WriteLine($"Rule Name: {rule.RuleName}"); + Console.WriteLine($"Rule ARN: {rule.RuleArn}"); + Console.WriteLine("--------------"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 11: Search things using the Thing name + Console.WriteLine(new string('-', 80)); + Console.WriteLine("10. Search things using the Thing name."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + if (searchResults.Any()) + { + Console.WriteLine($"Thing id found using search is {searchResults.First().ThingId}"); + } + else + { + Console.WriteLine($"No search results found for Thing: {thingName}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 12: Cleanup - Detach and delete certificate + if (!string.IsNullOrEmpty(certificateArn)) + { + Console.WriteLine(new string('-', 80)); + Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); + var deleteCert = Console.ReadLine(); + + if (deleteCert?.ToLower() == "y") + { + Console.WriteLine("11. You selected to detach and delete the certificate."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); + + await _iotWrapper.DeleteCertificateAsync(certificateId); + Console.WriteLine($"{certificateArn} was successfully deleted."); + } + Console.WriteLine(new string('-', 80)); + } + + // Step 13: Delete the AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("12. Delete the AWS IoT Thing."); + Console.Write($"Do you want to delete the IoT Thing? (y/n)"); + var deleteThing = Console.ReadLine(); + + if (deleteThing?.ToLower() == "y") + { + await _iotWrapper.DeleteThingAsync(thingName); + Console.WriteLine($"Deleted Thing {thingName}"); + } + Console.WriteLine(new string('-', 80)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred during scenario execution."); + + // Cleanup on error + if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) + { + try + { + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await _iotWrapper.DeleteCertificateAsync(certificateId); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during cleanup."); + } + } + + if (!string.IsNullOrEmpty(thingName)) + { + try + { + await _iotWrapper.DeleteThingAsync(thingName); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during Thing cleanup."); + } + } + + throw; + } + } +} +// snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj new file mode 100644 index 00000000000..bc870c03ebf --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs new file mode 100644 index 00000000000..1c52d1f813f --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -0,0 +1,163 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IotData; +using IoTActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace IoTTests; + +/// +/// Integration tests for the IoT wrapper methods. +/// +public class IoTIntegrationTests +{ + private readonly ITestOutputHelper _output; + private readonly IoTWrapper _iotWrapper; + + /// + /// Constructor for the test class. + /// + /// ITestOutputHelper object. + public IoTIntegrationTests(ITestOutputHelper output) + { + _output = output; + + // Set up dependency injection for the Amazon service. + var host = Host.CreateDefaultBuilder() + .ConfigureServices((_, services) => + services.AddAWSService() + .AddAWSService() + .AddTransient() + .AddLogging(builder => builder.AddConsole()) + ) + .Build(); + + _iotWrapper = host.Services.GetRequiredService(); + } + + /// + /// Test the IoT wrapper methods by running through the scenario. + /// + /// A Task object. + [Fact] + [Trait("Category", "Integration")] + public async Task IoTWrapperMethodsTest() + { + var thingName = $"test-thing-{Guid.NewGuid():N}"; + var certificateArn = ""; + var certificateId = ""; + + try + { + _output.WriteLine("Starting IoT integration test..."); + + // 1. Create an IoT Thing + _output.WriteLine($"Creating IoT Thing: {thingName}"); + var thingArn = await _iotWrapper.CreateThingAsync(thingName); + Assert.False(string.IsNullOrEmpty(thingArn)); + _output.WriteLine($"Created Thing with ARN: {thingArn}"); + + // 2. Create a certificate + _output.WriteLine("Creating device certificate..."); + var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + certificateArn = certArn; + certificateId = certId; + Assert.False(string.IsNullOrEmpty(certificateArn)); + Assert.False(string.IsNullOrEmpty(certPem)); + Assert.False(string.IsNullOrEmpty(certificateId)); + _output.WriteLine($"Created certificate with ARN: {certificateArn}"); + + // 3. Attach certificate to Thing + _output.WriteLine("Attaching certificate to Thing..."); + var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + Assert.True(attachResult); + + // 4. Update Thing with attributes + _output.WriteLine("Updating Thing attributes..."); + var attributes = new Dictionary + { + { "TestAttribute", "TestValue" }, + { "Environment", "Testing" } + }; + var updateResult = await _iotWrapper.UpdateThingAsync(thingName, attributes); + Assert.True(updateResult); + + // 5. Get IoT endpoint + _output.WriteLine("Getting IoT endpoint..."); + var endpoint = await _iotWrapper.DescribeEndpointAsync(); + Assert.False(string.IsNullOrEmpty(endpoint)); + _output.WriteLine($"Retrieved endpoint: {endpoint}"); + + // 6. List certificates + _output.WriteLine("Listing certificates..."); + var certificates = await _iotWrapper.ListCertificatesAsync(); + Assert.NotNull(certificates); + Assert.True(certificates.Count > 0); + _output.WriteLine($"Found {certificates.Count} certificates"); + + // 7. Update Thing shadow + _output.WriteLine("Updating Thing shadow..."); + var shadowPayload = """{"state": {"desired": {"temperature": 22, "humidity": 45}}}"""; + var shadowResult = await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + Assert.True(shadowResult); + + // 8. Get Thing shadow + _output.WriteLine("Getting Thing shadow..."); + var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + Assert.False(string.IsNullOrEmpty(shadowData)); + _output.WriteLine($"Retrieved shadow data: {shadowData}"); + + // 9. List topic rules + _output.WriteLine("Listing topic rules..."); + var rules = await _iotWrapper.ListTopicRulesAsync(); + Assert.NotNull(rules); + _output.WriteLine($"Found {rules.Count} IoT rules"); + + // 10. Search Things + _output.WriteLine("Searching for Things..."); + var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + Assert.NotNull(searchResults); + // Note: Search may not immediately return results for newly created Things + _output.WriteLine($"Search returned {searchResults.Count} results"); + + // 11. List Things + _output.WriteLine("Listing Things..."); + var things = await _iotWrapper.ListThingsAsync(); + Assert.NotNull(things); + Assert.True(things.Count > 0); + _output.WriteLine($"Found {things.Count} Things"); + + _output.WriteLine("IoT integration test completed successfully!"); + } + finally + { + // Cleanup resources + try + { + if (!string.IsNullOrEmpty(certificateArn)) + { + _output.WriteLine("Cleaning up: Detaching certificate from Thing..."); + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + + _output.WriteLine("Cleaning up: Deleting certificate..."); + await _iotWrapper.DeleteCertificateAsync(certificateId); + } + + _output.WriteLine("Cleaning up: Deleting Thing..."); + await _iotWrapper.DeleteThingAsync(thingName); + + _output.WriteLine("Cleanup completed successfully."); + } + catch (Exception ex) + { + _output.WriteLine($"Warning: Cleanup failed: {ex.Message}"); + } + } + } +} diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj new file mode 100644 index 00000000000..82596e09888 --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + From 1e4c7f4cbc947136e8e55511363da567ddf112a7 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:20:07 -0600 Subject: [PATCH 02/26] Update specification. --- .../basics/controltower/SPECIFICATION.md | 2 +- scenarios/basics/iot/SPECIFICATION.md | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scenarios/basics/controltower/SPECIFICATION.md b/scenarios/basics/controltower/SPECIFICATION.md index b4e9c8590dc..58ee1fbfdc4 100644 --- a/scenarios/basics/controltower/SPECIFICATION.md +++ b/scenarios/basics/controltower/SPECIFICATION.md @@ -210,7 +210,7 @@ Thanks for watching! ## Errors The following errors are handled in the Control Tower wrapper class: -| action | Error | Handling | +| Action | Error | Handling | |------------------------|-----------------------|------------------------------------------------------------------------| | `ListBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | | `ListEnabledBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 4b841154538..891e4123d60 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -68,6 +68,29 @@ This scenario demonstrates the following key AWS IoT Service operations: Note: We have buy off on these operations from IoT SME. +## Exception Handling + +Each AWS IoT operation can throw specific exceptions that should be handled appropriately. The following table lists the potential exceptions for each action: + +| Action | Error | Handling | +|------------------------|---------------------------------|------------------------------------------------------------------------| +| **CreateThing** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **CreateKeysAndCertificate** | ThrottlingException | Notify the user to try again later +| **AttachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **UpdateThing** | ResourceNotFoundException | Notify cannot perform action and return +| **DescribeEndpoint** | ThrottlingException | Notify the user to try again later +| **ListCertificates** | ThrottlingException | Notify the user to try again later +| **UpdateThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **GetThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **CreateTopicRule** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **ListTopicRules** | ThrottlingException | Notify the user to try again later +| **SearchIndex** | ThrottlingException | Notify the user to try again later +| **DetachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteCertificate** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteThing** | ResourceNotFoundException | Notify cannot perform action and return +| **ListThings** | ThrottlingException | Notify the user to try again later + + ### Program execution This scenario does have user interaction. The following shows the output of the program. @@ -220,5 +243,5 @@ The following table describes the metadata used in this scenario. | `updateThing` | iot_metadata.yaml | iot_UpdateThing | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | -| `hello ` | iot_metadata.yaml | iot_Hello | +| `hello` | iot_metadata.yaml | iot_Hello | | `scenario | iot_metadata.yaml | iot_Scenario | From df58ff05587e78044583ad9e0b7bf60c76d25d6a Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:38:28 -0600 Subject: [PATCH 03/26] Update exception handling. --- dotnetv4/IoT/Actions/HelloIoT.cs | 2 +- dotnetv4/IoT/Actions/IoTWrapper.cs | 173 ++++++++++++++++------ dotnetv4/IoT/Scenarios/IoTBasics.cs | 74 +++++---- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 4 +- 4 files changed, 176 insertions(+), 77 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index 60a61f9671f..de16511f44f 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -33,7 +33,7 @@ public static async Task Main(string[] args) var response = await iotClient.ListThingsAsync(request); - if (response.Things.Count > 0) + if (response.Things is { Count: > 0 }) { Console.WriteLine($"Found {response.Things.Count} IoT Things:"); foreach (var thing in response.Things) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index e070dd27233..d0d61b0ed3f 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -38,8 +38,8 @@ public IoTWrapper(IAmazonIoT amazonIoT, IAmazonIotData amazonIotData, ILogger /// The name of the Thing to create. - /// The ARN of the Thing created. - public async Task CreateThingAsync(string thingName) + /// The ARN of the Thing created, or null if creation failed. + public async Task CreateThingAsync(string thingName) { try { @@ -52,10 +52,15 @@ public async Task CreateThingAsync(string thingName) _logger.LogInformation($"Created Thing {thingName} with ARN {response.ThingArn}"); return response.ThingArn; } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Thing {thingName} already exists: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error creating Thing {thingName}: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create Thing {thingName}. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.CreateThing] @@ -64,8 +69,8 @@ public async Task CreateThingAsync(string thingName) /// /// Creates a device certificate for AWS IoT. /// - /// The certificate details including ARN and certificate PEM. - public async Task<(string CertificateArn, string CertificatePem, string CertificateId)> CreateKeysAndCertificateAsync() + /// The certificate details including ARN and certificate PEM, or null if creation failed. + public async Task<(string CertificateArn, string CertificatePem, string CertificateId)?> CreateKeysAndCertificateAsync() { try { @@ -78,10 +83,15 @@ public async Task CreateThingAsync(string thingName) _logger.LogInformation($"Created certificate with ARN {response.CertificateArn}"); return (response.CertificateArn, response.CertificatePem, response.CertificateId); } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error creating certificate: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create certificate. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.CreateKeysAndCertificate] @@ -92,7 +102,7 @@ public async Task CreateThingAsync(string thingName) /// /// The name of the Thing. /// The ARN of the certificate to attach. - /// True if successful. + /// True if successful, false otherwise. public async Task AttachThingPrincipalAsync(string thingName, string certificateArn) { try @@ -107,10 +117,15 @@ public async Task AttachThingPrincipalAsync(string thingName, string certi _logger.LogInformation($"Attached certificate {certificateArn} to Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot attach certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error attaching certificate to Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't attach certificate to Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.AttachThingPrincipal] @@ -121,7 +136,7 @@ public async Task AttachThingPrincipalAsync(string thingName, string certi /// /// The name of the Thing to update. /// Dictionary of attributes to add. - /// True if successful. + /// True if successful, false otherwise. public async Task UpdateThingAsync(string thingName, Dictionary attributes) { try @@ -140,10 +155,15 @@ public async Task UpdateThingAsync(string thingName, Dictionary UpdateThingAsync(string thingName, Dictionary /// Gets the AWS IoT endpoint URL. /// - /// The endpoint URL. - public async Task DescribeEndpointAsync() + /// The endpoint URL, or null if retrieval failed. + public async Task DescribeEndpointAsync() { try { @@ -166,10 +186,15 @@ public async Task DescribeEndpointAsync() _logger.LogInformation($"Retrieved endpoint: {response.EndpointAddress}"); return response.EndpointAddress; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error describing endpoint: {ex.Message}"); - throw; + _logger.LogError($"Couldn't describe endpoint. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.DescribeEndpoint] @@ -178,7 +203,7 @@ public async Task DescribeEndpointAsync() /// /// Lists all certificates associated with the account. /// - /// List of certificate information. + /// List of certificate information, or empty list if listing failed. public async Task> ListCertificatesAsync() { try @@ -189,10 +214,15 @@ public async Task> ListCertificatesAsync() _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); return response.Certificates; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing certificates: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list certificates. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListCertificates] @@ -203,7 +233,7 @@ public async Task> ListCertificatesAsync() /// /// The name of the Thing. /// The shadow payload in JSON format. - /// True if successful. + /// True if successful, false otherwise. public async Task UpdateThingShadowAsync(string thingName, string shadowPayload) { try @@ -218,10 +248,15 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa _logger.LogInformation($"Updated shadow for Thing {thingName}"); return true; } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot update Thing shadow - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error updating Thing shadow: {ex.Message}"); - throw; + _logger.LogError($"Couldn't update Thing shadow. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.UpdateThingShadow] @@ -231,8 +266,8 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa /// Gets the Thing's shadow information. /// /// The name of the Thing. - /// The shadow data as a string. - public async Task GetThingShadowAsync(string thingName) + /// The shadow data as a string, or null if retrieval failed. + public async Task GetThingShadowAsync(string thingName) { try { @@ -248,10 +283,15 @@ public async Task GetThingShadowAsync(string thingName) _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); return shadowData; } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot get Thing shadow - resource not found: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error getting Thing shadow: {ex.Message}"); - throw; + _logger.LogError($"Couldn't get Thing shadow. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.GetThingShadow] @@ -263,7 +303,7 @@ public async Task GetThingShadowAsync(string thingName) /// The name of the rule. /// The ARN of the SNS topic for the action. /// The ARN of the IAM role. - /// True if successful. + /// True if successful, false otherwise. public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn, string roleArn) { try @@ -294,10 +334,15 @@ public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn _logger.LogInformation($"Created IoT rule {ruleName}"); return true; } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Rule {ruleName} already exists: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error creating topic rule: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create topic rule. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.CreateTopicRule] @@ -306,7 +351,7 @@ public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn /// /// Lists all IoT topic rules. /// - /// List of topic rules. + /// List of topic rules, or empty list if listing failed. public async Task> ListTopicRulesAsync() { try @@ -317,10 +362,15 @@ public async Task> ListTopicRulesAsync() _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); return response.Rules; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing topic rules: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list topic rules. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListTopicRules] @@ -330,7 +380,7 @@ public async Task> ListTopicRulesAsync() /// Searches for IoT Things using the search index. /// /// The search query string. - /// List of Things that match the search criteria. + /// List of Things that match the search criteria, or empty list if search failed. public async Task> SearchIndexAsync(string queryString) { try @@ -345,10 +395,15 @@ public async Task> SearchIndexAsync(string queryString) _logger.LogInformation($"Search found {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error searching index: {ex.Message}"); - throw; + _logger.LogError($"Couldn't search index. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.SearchIndex] @@ -359,7 +414,7 @@ public async Task> SearchIndexAsync(string queryString) /// /// The name of the Thing. /// The ARN of the certificate to detach. - /// True if successful. + /// True if successful, false otherwise. public async Task DetachThingPrincipalAsync(string thingName, string certificateArn) { try @@ -374,10 +429,15 @@ public async Task DetachThingPrincipalAsync(string thingName, string certi _logger.LogInformation($"Detached certificate {certificateArn} from Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot detach certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error detaching certificate from Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't detach certificate from Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DetachThingPrincipal] @@ -387,7 +447,7 @@ public async Task DetachThingPrincipalAsync(string thingName, string certi /// Deletes an IoT certificate. /// /// The ID of the certificate to delete. - /// True if successful. + /// True if successful, false otherwise. public async Task DeleteCertificateAsync(string certificateId) { try @@ -410,10 +470,15 @@ public async Task DeleteCertificateAsync(string certificateId) _logger.LogInformation($"Deleted certificate {certificateId}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error deleting certificate: {ex.Message}"); - throw; + _logger.LogError($"Couldn't delete certificate. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DeleteCertificate] @@ -423,7 +488,7 @@ public async Task DeleteCertificateAsync(string certificateId) /// Deletes an IoT Thing. /// /// The name of the Thing to delete. - /// True if successful. + /// True if successful, false otherwise. public async Task DeleteThingAsync(string thingName) { try @@ -437,10 +502,15 @@ public async Task DeleteThingAsync(string thingName) _logger.LogInformation($"Deleted Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete Thing - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error deleting Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't delete Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DeleteThing] @@ -449,7 +519,7 @@ public async Task DeleteThingAsync(string thingName) /// /// Lists IoT Things with pagination support. /// - /// List of Things. + /// List of Things, or empty list if listing failed. public async Task> ListThingsAsync() { try @@ -460,10 +530,15 @@ public async Task> ListThingsAsync() _logger.LogInformation($"Retrieved {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing Things: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list Things. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListThings] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index da7a2243b95..622f82860c1 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -106,33 +106,48 @@ private static async Task RunScenarioAsync() if (createCert?.ToLower() == "y") { - var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); - certificateArn = certArn; - certificateId = certId; - - Console.WriteLine($"\nCertificate:"); - // Show only first few lines of certificate for brevity - var lines = certPem.Split('\n'); - for (int i = 0; i < Math.Min(lines.Length, 5); i++) + var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + if (certificateResult.HasValue) { - Console.WriteLine(lines[i]); - } - if (lines.Length > 5) - { - Console.WriteLine("..."); - } + var (certArn, certPem, certId) = certificateResult.Value; + certificateArn = certArn; + certificateId = certId; + + Console.WriteLine($"\nCertificate:"); + // Show only first few lines of certificate for brevity + var lines = certPem.Split('\n'); + for (int i = 0; i < Math.Min(lines.Length, 5); i++) + { + Console.WriteLine(lines[i]); + } + if (lines.Length > 5) + { + Console.WriteLine("..."); + } - Console.WriteLine($"\nCertificate ARN:"); - Console.WriteLine(certificateArn); + Console.WriteLine($"\nCertificate ARN:"); + Console.WriteLine(certificateArn); - // Step 3: Attach the Certificate to the AWS IoT Thing - Console.WriteLine("Attach the certificate to the AWS IoT Thing."); - await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); - Console.WriteLine("Certificate attached to Thing successfully."); + // Step 3: Attach the Certificate to the AWS IoT Thing + Console.WriteLine("Attach the certificate to the AWS IoT Thing."); + var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + if (attachResult) + { + Console.WriteLine("Certificate attached to Thing successfully."); + } + else + { + Console.WriteLine("Failed to attach certificate to Thing."); + } - Console.WriteLine("Thing Details:"); - Console.WriteLine($"Thing Name: {thingName}"); - Console.WriteLine($"Thing ARN: {thingArn}"); + Console.WriteLine("Thing Details:"); + Console.WriteLine($"Thing Name: {thingName}"); + Console.WriteLine($"Thing ARN: {thingArn}"); + } + else + { + Console.WriteLine("Failed to create certificate."); + } } Console.WriteLine(new string('-', 80)); @@ -165,9 +180,16 @@ private static async Task RunScenarioAsync() Console.ReadLine(); var endpoint = await _iotWrapper.DescribeEndpointAsync(); - var subdomain = endpoint.Split('.')[0]; - Console.WriteLine($"Extracted subdomain: {subdomain}"); - Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + if (endpoint != null) + { + var subdomain = endpoint.Split('.')[0]; + Console.WriteLine($"Extracted subdomain: {subdomain}"); + Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + } + else + { + Console.WriteLine("Failed to retrieve endpoint."); + } Console.WriteLine(new string('-', 80)); // Step 6: List your AWS IoT certificates diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index 1c52d1f813f..b164a8f90c3 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -65,7 +65,9 @@ public async Task IoTWrapperMethodsTest() // 2. Create a certificate _output.WriteLine("Creating device certificate..."); - var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + Assert.True(certificateResult.HasValue); + var (certArn, certPem, certId) = certificateResult.Value; certificateArn = certArn; certificateId = certId; Assert.False(string.IsNullOrEmpty(certificateArn)); From cc18e640cac723ea408a8b65985ea261959c17d4 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:26:01 -0600 Subject: [PATCH 04/26] Updates to classes and specification --- dotnetv4/IoT/Actions/IoTWrapper.cs | 2 +- dotnetv4/IoT/Scenarios/IoTBasics.cs | 392 +++++++++++++++++++--- dotnetv4/IoT/Scenarios/IoTBasics.csproj | 2 + dotnetv4/IoT/Scenarios/appsettings.json | 11 + dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 4 + dotnetv4/IoT/Tests/IoTTests.csproj | 1 + scenarios/basics/iot/SPECIFICATION.md | 8 +- 7 files changed, 369 insertions(+), 51 deletions(-) create mode 100644 dotnetv4/IoT/Scenarios/appsettings.json diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index d0d61b0ed3f..352718ec3da 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -6,7 +6,6 @@ using Amazon.IotData; using Amazon.IotData.Model; using Microsoft.Extensions.Logging; -using System.Text.Json; namespace IoTActions; @@ -542,5 +541,6 @@ public async Task> ListThingsAsync() } } // snippet-end:[iot.dotnetv4.ListThings] + } // snippet-end:[iot.dotnetv4.IoTWrapper] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 622f82860c1..a96050ddf3e 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -1,14 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Text.Json; +using Amazon; +using Amazon.Extensions.NETCore.Setup; +using Amazon.IdentityManagement; using Amazon.IoT; +//using Amazon.IoT.Model; using Amazon.IotData; -using Amazon.IoT.Model; +using Amazon.SimpleNotificationService; using IoTActions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Text.Json; namespace IoTScenarios; @@ -18,7 +23,10 @@ namespace IoTScenarios; /// public class IoTBasics { + public static bool IsInteractive = true; private static IoTWrapper _iotWrapper = null!; + private static IAmazonSimpleNotificationService _amazonSNS = null!; + private static IAmazonIdentityManagementService _amazonIAM = null!; private static ILogger _logger = null!; /// @@ -28,20 +36,35 @@ public class IoTBasics /// A Task object. public static async Task Main(string[] args) { + //var config = new ConfigurationBuilder() + // .AddJsonFile("appsettings.json") + // .Build(); + // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => - services.AddAWSService() - .AddAWSService() + services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) + //.AddAWSService(new AWSOptions(){DefaultClientConfig = new AmazonIotDataConfig(){ServiceURL = "https://data.iot.us-east-1.amazonaws.com/"}}) + .AddAWSService() + .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) + .AddSingleton(sp => + { + return new AmazonIotDataClient( + "https://data.iot.us-east-1.amazonaws.com/"); + }) ) .Build(); + + _logger = LoggerFactory.Create(builder => builder.AddConsole()) .CreateLogger(); _iotWrapper = host.Services.GetRequiredService(); + _amazonSNS = host.Services.GetRequiredService(); + _amazonIAM = host.Services.GetRequiredService(); Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); @@ -52,8 +75,11 @@ public static async Task Main(string[] args) Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); Console.WriteLine("developers working with AWS IoT in a .NET environment."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } Console.WriteLine(new string('-', 80)); try @@ -77,10 +103,12 @@ public static async Task Main(string[] args) /// A Task object. private static async Task RunScenarioAsync() { - string thingName = ""; + string thingName = $"iot-thing-{Guid.NewGuid():N}"; string certificateArn = ""; string certificateId = ""; - string ruleName = ""; + string ruleName = $"iot-rule-{Guid.NewGuid():N}"; + string snsTopicArn = ""; + string iotRoleName = ""; try { @@ -89,8 +117,18 @@ private static async Task RunScenarioAsync() Console.WriteLine("1. Create an AWS IoT Thing."); Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); Console.WriteLine(); - Console.Write("Enter Thing name: "); - thingName = Console.ReadLine()!; + + if (IsInteractive) + { + Console.Write("Enter Thing name: "); + var userInput = Console.ReadLine(); + if (!string.IsNullOrEmpty(userInput)) + thingName = userInput; + } + else + { + Console.WriteLine($"Using default Thing name: {thingName}"); + } var thingArn = await _iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); @@ -101,8 +139,17 @@ private static async Task RunScenarioAsync() Console.WriteLine("2. Generate a device certificate."); Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); Console.WriteLine(); - Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); - var createCert = Console.ReadLine(); + + var createCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); + createCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Creating certificate for {thingName}..."); + } if (createCert?.ToLower() == "y") { @@ -157,8 +204,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var attributes = new Dictionary { @@ -176,8 +226,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("4. Return a unique endpoint specific to the Amazon Web Services account."); Console.WriteLine("An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var endpoint = await _iotWrapper.DescribeEndpointAsync(); if (endpoint != null) @@ -195,8 +248,11 @@ private static async Task RunScenarioAsync() // Step 6: List your AWS IoT certificates Console.WriteLine(new string('-', 80)); Console.WriteLine("5. List your AWS IoT certificates"); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var certificates = await _iotWrapper.ListCertificatesAsync(); foreach (var cert in certificates.Take(5)) // Show first 5 certificates @@ -214,8 +270,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between"); Console.WriteLine("the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var shadowPayload = JsonSerializer.Serialize(new { @@ -236,46 +295,75 @@ private static async Task RunScenarioAsync() // Step 8: Write out the state information, in JSON format Console.WriteLine(new string('-', 80)); Console.WriteLine("7. Write out the state information, in JSON format."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); Console.WriteLine($"Received Shadow Data: {shadowData}"); Console.WriteLine(new string('-', 80)); - // Step 9: Creates a rule + // Step 9: Set up resources (SNS topic and IAM role) and create a rule Console.WriteLine(new string('-', 80)); - Console.WriteLine("8. Creates a rule"); + Console.WriteLine("8. Set up resources and create a rule"); Console.WriteLine("Creates a rule that is an administrator-level action."); Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); Console.WriteLine(); - Console.Write("Enter Rule name: "); - ruleName = Console.ReadLine()!; + + if (IsInteractive) + { + Console.Write("Enter Rule name: "); + var userRuleName = Console.ReadLine(); + if (!string.IsNullOrEmpty(userRuleName)) + ruleName = userRuleName; + } + else + { + Console.WriteLine($"Using default rule name: {ruleName}"); + } - // Note: For demonstration, we'll use placeholder ARNs - // In real usage, these should be actual SNS topic and IAM role ARNs - var snsTopicArn = "arn:aws:sns:us-east-1:123456789012:example-topic"; - var roleArn = "arn:aws:iam::123456789012:role/IoTRole"; + // Set up SNS topic and IAM role for the IoT rule + var topicName = $"iot-topic-{Guid.NewGuid():N}"; + iotRoleName = $"iot-role-{Guid.NewGuid():N}"; - Console.WriteLine("Note: Using placeholder ARNs for SNS topic and IAM role."); - Console.WriteLine("In production, ensure these ARNs exist and have proper permissions."); + Console.WriteLine("Setting up SNS topic and IAM role for the IoT rule..."); + var setupResult = await SetupAsync(topicName, iotRoleName); - try + string roleArn = ""; + + if (setupResult.HasValue) { - await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); - Console.WriteLine("IoT Rule created successfully."); + (snsTopicArn, roleArn) = setupResult.Value; + Console.WriteLine($"Successfully created SNS topic: {snsTopicArn}"); + Console.WriteLine($"Successfully created IAM role: {roleArn}"); + + // Now create the IoT rule with the actual ARNs + var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + if (ruleResult) + { + Console.WriteLine("IoT Rule created successfully."); + } + else + { + Console.WriteLine("Failed to create IoT rule."); + } } - catch (Exception ex) + else { - Console.WriteLine($"Note: Rule creation failed (expected with placeholder ARNs): {ex.Message}"); + Console.WriteLine("Failed to set up SNS topic and IAM role. Skipping rule creation."); } Console.WriteLine(new string('-', 80)); // Step 10: List your rules Console.WriteLine(new string('-', 80)); Console.WriteLine("9. List your rules."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var rules = await _iotWrapper.ListTopicRulesAsync(); Console.WriteLine("List of IoT Rules:"); @@ -291,8 +379,11 @@ private static async Task RunScenarioAsync() // Step 11: Search things using the Thing name Console.WriteLine(new string('-', 80)); Console.WriteLine("10. Search things using the Thing name."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); if (searchResults.Any()) @@ -309,14 +400,25 @@ private static async Task RunScenarioAsync() if (!string.IsNullOrEmpty(certificateArn)) { Console.WriteLine(new string('-', 80)); - Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); - var deleteCert = Console.ReadLine(); + var deleteCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); + deleteCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Detaching and deleting certificate for {thingName}..."); + } if (deleteCert?.ToLower() == "y") { Console.WriteLine("11. You selected to detach and delete the certificate."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); @@ -330,8 +432,16 @@ private static async Task RunScenarioAsync() // Step 13: Delete the AWS IoT Thing Console.WriteLine(new string('-', 80)); Console.WriteLine("12. Delete the AWS IoT Thing."); - Console.Write($"Do you want to delete the IoT Thing? (y/n)"); - var deleteThing = Console.ReadLine(); + var deleteThing = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to delete the IoT Thing? (y/n)"); + deleteThing = Console.ReadLine(); + } + else + { + Console.WriteLine($"Deleting IoT Thing {thingName}..."); + } if (deleteThing?.ToLower() == "y") { @@ -339,6 +449,25 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Deleted Thing {thingName}"); } Console.WriteLine(new string('-', 80)); + + // Step 14: Clean up SNS topic and IAM role + if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine("13. Clean up SNS topic and IAM role."); + Console.WriteLine("Cleaning up the resources created for the IoT rule..."); + + var cleanupSuccess = await CleanupAsync(snsTopicArn, iotRoleName); + if (cleanupSuccess) + { + Console.WriteLine("Successfully cleaned up SNS topic and IAM role."); + } + else + { + Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + } + Console.WriteLine(new string('-', 80)); + } } catch (Exception ex) { @@ -369,9 +498,178 @@ private static async Task RunScenarioAsync() _logger.LogError(cleanupEx, "Error during Thing cleanup."); } } + + // Clean up SNS topic and IAM role on error + if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + { + try + { + await CleanupAsync(snsTopicArn, iotRoleName); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during SNS and IAM cleanup."); + } + } throw; } } + + // snippet-start:[iot.dotnetv4.Setup] + /// + /// Sets up the necessary resources for the IoT scenario (SNS topic and IAM role). + /// + /// The name of the SNS topic to create. + /// The name of the IAM role to create. + /// A tuple containing the SNS topic ARN and IAM role ARN, or null if setup failed. + private static async Task<(string SnsTopicArn, string RoleArn)?> SetupAsync(string topicName, string roleName) + { + try + { + // Create SNS topic + var createTopicRequest = new Amazon.SimpleNotificationService.Model.CreateTopicRequest + { + Name = topicName + }; + + var topicResponse = await _amazonSNS.CreateTopicAsync(createTopicRequest); + var snsTopicArn = topicResponse.TopicArn; + _logger.LogInformation($"Created SNS topic {topicName} with ARN {snsTopicArn}"); + + // Create IAM role for IoT + var trustPolicy = @"{ + ""Version"": ""2012-10-17"", + ""Statement"": [ + { + ""Effect"": ""Allow"", + ""Principal"": { + ""Service"": ""iot.amazonaws.com"" + }, + ""Action"": ""sts:AssumeRole"" + } + ] + }"; + + var createRoleRequest = new Amazon.IdentityManagement.Model.CreateRoleRequest + { + RoleName = roleName, + AssumeRolePolicyDocument = trustPolicy, + Description = "Role for AWS IoT to publish to SNS topic" + }; + + var roleResponse = await _amazonIAM.CreateRoleAsync(createRoleRequest); + var roleArn = roleResponse.Role.Arn; + _logger.LogInformation($"Created IAM role {roleName} with ARN {roleArn}"); + + // Attach policy to allow SNS publishing + var policyDocument = $@"{{ + ""Version"": ""2012-10-17"", + ""Statement"": [ + {{ + ""Effect"": ""Allow"", + ""Action"": ""sns:Publish"", + ""Resource"": ""{snsTopicArn}"" + }} + ] + }}"; + + var putRolePolicyRequest = new Amazon.IdentityManagement.Model.PutRolePolicyRequest + { + RoleName = roleName, + PolicyName = "IoTSNSPolicy", + PolicyDocument = policyDocument + }; + + await _amazonIAM.PutRolePolicyAsync(putRolePolicyRequest); + _logger.LogInformation($"Attached SNS policy to role {roleName}"); + + // Wait a bit for the role to propagate + await Task.Delay(10000); + + return (snsTopicArn, roleArn); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't set up resources. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.Setup] + + // snippet-start:[iot.dotnetv4.Cleanup] + /// + /// Cleans up the resources created during setup (SNS topic and IAM role). + /// + /// The ARN of the SNS topic to delete. + /// The name of the IAM role to delete. + /// True if cleanup was successful, false otherwise. + private static async Task CleanupAsync(string snsTopicArn, string roleName) + { + var success = true; + + try + { + // Delete role policy first + try + { + var deleteRolePolicyRequest = new Amazon.IdentityManagement.Model.DeleteRolePolicyRequest + { + RoleName = roleName, + PolicyName = "IoTSNSPolicy" + }; + + await _amazonIAM.DeleteRolePolicyAsync(deleteRolePolicyRequest); + _logger.LogInformation($"Deleted role policy for {roleName}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete role policy: {ex.Message}"); + success = false; + } + + // Delete IAM role + try + { + var deleteRoleRequest = new Amazon.IdentityManagement.Model.DeleteRoleRequest + { + RoleName = roleName + }; + + await _amazonIAM.DeleteRoleAsync(deleteRoleRequest); + _logger.LogInformation($"Deleted IAM role {roleName}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete IAM role: {ex.Message}"); + success = false; + } + + // Delete SNS topic + try + { + var deleteTopicRequest = new Amazon.SimpleNotificationService.Model.DeleteTopicRequest + { + TopicArn = snsTopicArn + }; + + await _amazonSNS.DeleteTopicAsync(deleteTopicRequest); + _logger.LogInformation($"Deleted SNS topic {snsTopicArn}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete SNS topic: {ex.Message}"); + success = false; + } + + return success; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't clean up resources. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.Cleanup] } // snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index bc870c03ebf..41a0573fb20 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -10,6 +10,8 @@ + + diff --git a/dotnetv4/IoT/Scenarios/appsettings.json b/dotnetv4/IoT/Scenarios/appsettings.json new file mode 100644 index 00000000000..a84bda6ba5b --- /dev/null +++ b/dotnetv4/IoT/Scenarios/appsettings.json @@ -0,0 +1,11 @@ +{ + "AwsIotDataConfig": { + "Profile": "default", + "ServiceURL": "https://data.iot.us-east-1.amazonaws.com/" + }, + + "AwsConfig": { + "Profile": "default", + "Region": "us-east-1" + } +} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index b164a8f90c3..f5c4728218f 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -4,6 +4,7 @@ using Amazon.IoT; using Amazon.IotData; using IoTActions; +using IoTScenarios; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -49,6 +50,9 @@ public IoTIntegrationTests(ITestOutputHelper output) [Trait("Category", "Integration")] public async Task IoTWrapperMethodsTest() { + // Set to non-interactive mode for testing + IoTScenarios.IoTBasics.IsInteractive = false; + var thingName = $"test-thing-{Guid.NewGuid():N}"; var certificateArn = ""; var certificateId = ""; diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index 82596e09888..ed1b6b951ca 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -29,6 +29,7 @@ + diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 891e4123d60..bc7aaa6efe0 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,10 +6,12 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program requires these AWS resources. +This program should create and manage these AWS resources automatically: -1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. -2. **snsAction** - An ARN of an SNS topic. +1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. This role must be automatically created during the scenario execution with proper permissions to publish to SNS topics. +2. **snsAction** - An ARN of an SNS topic. This topic must be automatically created during the scenario execution for use with IoT rules. + +Both resources must be created during scenario setup and automatically cleaned up at the end of the scenario execution. ## Hello AWS IoT From 0d0ddf9a6cffa1834cbb47e60e57d9fbc79fa16d Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:39:25 -0600 Subject: [PATCH 05/26] Updates to setup and basics. --- dotnetv4/IoT/Actions/IoTWrapper.cs | 10 +- dotnetv4/IoT/Scenarios/IoTBasics.cs | 424 ++++++++++++++-------- dotnetv4/IoT/Scenarios/IoTBasics.csproj | 1 + dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 3 +- scenarios/basics/iot/SPECIFICATION.md | 14 +- 5 files changed, 287 insertions(+), 165 deletions(-) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 352718ec3da..738cd8ac985 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -384,9 +384,17 @@ public async Task> SearchIndexAsync(string queryString) { try { + await _amazonIoT.UpdateIndexingConfigurationAsync( + new UpdateIndexingConfigurationRequest() + { + ThingIndexingConfiguration = new ThingIndexingConfiguration() + { + ThingIndexingMode = ThingIndexingMode.REGISTRY + } + }); + var request = new SearchIndexRequest { - IndexName = "AWS_Things", QueryString = queryString }; diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index a96050ddf3e..2aa59af617a 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -3,19 +3,18 @@ using System.Text.Json; using Amazon; +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; using Amazon.Extensions.NETCore.Setup; -using Amazon.IdentityManagement; using Amazon.IoT; -//using Amazon.IoT.Model; +using Amazon.IoT.Model; using Amazon.IotData; -using Amazon.SimpleNotificationService; using IoTActions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace IoTScenarios; +namespace IoTBasics; // snippet-start:[iot.dotnetv4.IoTScenario] /// @@ -25,10 +24,12 @@ public class IoTBasics { public static bool IsInteractive = true; private static IoTWrapper _iotWrapper = null!; - private static IAmazonSimpleNotificationService _amazonSNS = null!; - private static IAmazonIdentityManagementService _amazonIAM = null!; + private static IAmazonCloudFormation _amazonCloudFormation = null!; private static ILogger _logger = null!; + private static string _stackName = "IoTBasicsStack"; + private static string _stackResourcePath = "../../../../../../scenarios/basics/iot/iot_usecase/resources/cfn_template.yaml"; + /// /// Main method for the IoT Basics scenario. /// @@ -44,15 +45,18 @@ public static async Task Main(string[] args) using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) - //.AddAWSService(new AWSOptions(){DefaultClientConfig = new AmazonIotDataConfig(){ServiceURL = "https://data.iot.us-east-1.amazonaws.com/"}}) - .AddAWSService() - .AddAWSService() + .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) .AddSingleton(sp => { - return new AmazonIotDataClient( - "https://data.iot.us-east-1.amazonaws.com/"); + var iotService = sp.GetService(); + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + var response = iotService.DescribeEndpointAsync(request).Result; + return new AmazonIotDataClient($"https://{response.EndpointAddress}/"); }) ) .Build(); @@ -63,17 +67,11 @@ public static async Task Main(string[] args) .CreateLogger(); _iotWrapper = host.Services.GetRequiredService(); - _amazonSNS = host.Services.GetRequiredService(); - _amazonIAM = host.Services.GetRequiredService(); + _amazonCloudFormation = host.Services.GetRequiredService(); Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); - Console.WriteLine("The program guides you through a series of steps, including creating an IoT Thing, generating a device certificate,"); - Console.WriteLine("updating the Thing with attributes, and so on. It utilizes the AWS SDK for .NET and incorporates functionalities"); - Console.WriteLine("for creating and managing IoT Things, certificates, rules, shadows, and performing searches."); - Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); - Console.WriteLine("developers working with AWS IoT in a .NET environment."); Console.WriteLine(); if (IsInteractive) { @@ -108,7 +106,6 @@ private static async Task RunScenarioAsync() string certificateId = ""; string ruleName = $"iot-rule-{Guid.NewGuid():N}"; string snsTopicArn = ""; - string iotRoleName = ""; try { @@ -324,35 +321,52 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Using default rule name: {ruleName}"); } - // Set up SNS topic and IAM role for the IoT rule - var topicName = $"iot-topic-{Guid.NewGuid():N}"; - iotRoleName = $"iot-role-{Guid.NewGuid():N}"; - - Console.WriteLine("Setting up SNS topic and IAM role for the IoT rule..."); - var setupResult = await SetupAsync(topicName, iotRoleName); + // Deploy CloudFormation stack to create SNS topic and IAM role + Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); - string roleArn = ""; - - if (setupResult.HasValue) + var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); + if (deployStack) { - (snsTopicArn, roleArn) = setupResult.Value; - Console.WriteLine($"Successfully created SNS topic: {snsTopicArn}"); - Console.WriteLine($"Successfully created IAM role: {roleArn}"); - - // Now create the IoT rule with the actual ARNs - var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); - if (ruleResult) + _stackName = PromptUserForStackName(); + + var deploySuccess = await DeployCloudFormationStack(_stackName); + + if (deploySuccess) { - Console.WriteLine("IoT Rule created successfully."); + // Get stack outputs + var stackOutputs = await GetStackOutputs(_stackName); + if (stackOutputs != null) + { + snsTopicArn = stackOutputs["SNSTopicArn"]; + string roleArn = stackOutputs["RoleArn"]; + + Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); + Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); + + // Now create the IoT rule with the CloudFormation outputs + var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + if (ruleResult) + { + Console.WriteLine("IoT Rule created successfully."); + } + else + { + Console.WriteLine("Failed to create IoT rule."); + } + } + else + { + Console.WriteLine("Failed to get stack outputs. Skipping rule creation."); + } } else { - Console.WriteLine("Failed to create IoT rule."); + Console.WriteLine("Failed to deploy CloudFormation stack. Skipping rule creation."); } } else { - Console.WriteLine("Failed to set up SNS topic and IAM role. Skipping rule creation."); + Console.WriteLine("Skipping CloudFormation stack deployment and rule creation."); } Console.WriteLine(new string('-', 80)); @@ -450,21 +464,29 @@ private static async Task RunScenarioAsync() } Console.WriteLine(new string('-', 80)); - // Step 14: Clean up SNS topic and IAM role - if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + // Step 14: Clean up CloudFormation stack + if (!string.IsNullOrEmpty(snsTopicArn)) { Console.WriteLine(new string('-', 80)); - Console.WriteLine("13. Clean up SNS topic and IAM role."); - Console.WriteLine("Cleaning up the resources created for the IoT rule..."); + Console.WriteLine("13. Clean up CloudFormation stack."); + Console.WriteLine("Deleting the CloudFormation stack and all resources..."); - var cleanupSuccess = await CleanupAsync(snsTopicArn, iotRoleName); - if (cleanupSuccess) + var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); + if (cleanup) { - Console.WriteLine("Successfully cleaned up SNS topic and IAM role."); + var cleanupSuccess = await DeleteCloudFormationStack(_stackName); + if (cleanupSuccess) + { + Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); + } + else + { + Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + } } else { - Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + Console.WriteLine($"Resources will remain. Stack name: {_stackName}"); } Console.WriteLine(new string('-', 80)); } @@ -499,16 +521,16 @@ private static async Task RunScenarioAsync() } } - // Clean up SNS topic and IAM role on error - if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + // Clean up CloudFormation stack on error + if (!string.IsNullOrEmpty(snsTopicArn)) { try { - await CleanupAsync(snsTopicArn, iotRoleName); + await DeleteCloudFormationStack(_stackName); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during SNS and IAM cleanup."); + _logger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } @@ -516,160 +538,246 @@ private static async Task RunScenarioAsync() } } - // snippet-start:[iot.dotnetv4.Setup] /// - /// Sets up the necessary resources for the IoT scenario (SNS topic and IAM role). + /// Deploys the CloudFormation stack with the necessary resources. /// - /// The name of the SNS topic to create. - /// The name of the IAM role to create. - /// A tuple containing the SNS topic ARN and IAM role ARN, or null if setup failed. - private static async Task<(string SnsTopicArn, string RoleArn)?> SetupAsync(string topicName, string roleName) + /// The name of the CloudFormation stack. + /// True if the stack was deployed successfully. + private static async Task DeployCloudFormationStack(string stackName) { + Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); + try { - // Create SNS topic - var createTopicRequest = new Amazon.SimpleNotificationService.Model.CreateTopicRequest + var request = new CreateStackRequest { - Name = topicName + StackName = stackName, + TemplateBody = await File.ReadAllTextAsync(_stackResourcePath), + Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } }; - var topicResponse = await _amazonSNS.CreateTopicAsync(createTopicRequest); - var snsTopicArn = topicResponse.TopicArn; - _logger.LogInformation($"Created SNS topic {topicName} with ARN {snsTopicArn}"); + var response = await _amazonCloudFormation.CreateStackAsync(request); - // Create IAM role for IoT - var trustPolicy = @"{ - ""Version"": ""2012-10-17"", - ""Statement"": [ - { - ""Effect"": ""Allow"", - ""Principal"": { - ""Service"": ""iot.amazonaws.com"" - }, - ""Action"": ""sts:AssumeRole"" - } - ] - }"; + if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + Console.WriteLine($"CloudFormation stack creation started: {stackName}"); + + bool stackCreated = await WaitForStackCompletion(response.StackId); + + if (stackCreated) + { + Console.WriteLine("CloudFormation stack created successfully."); + return true; + } + else + { + _logger.LogError($"CloudFormation stack creation failed: {stackName}"); + return false; + } + } + else + { + _logger.LogError($"Failed to create CloudFormation stack: {stackName}"); + return false; + } + } + catch (AlreadyExistsException) + { + _logger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); + var newStackName = PromptUserForStackName(); + return await DeployCloudFormationStack(newStackName); + } + catch (Exception ex) + { + _logger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); + return false; + } + } - var createRoleRequest = new Amazon.IdentityManagement.Model.CreateRoleRequest + /// + /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. + /// + /// The ID of the CloudFormation stack. + /// True if the stack was created successfully. + private static async Task WaitForStackCompletion(string stackId) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; + + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest { - RoleName = roleName, - AssumeRolePolicyDocument = trustPolicy, - Description = "Role for AWS IoT to publish to SNS topic" + StackName = stackId }; - var roleResponse = await _amazonIAM.CreateRoleAsync(createRoleRequest); - var roleArn = roleResponse.Role.Arn; - _logger.LogInformation($"Created IAM role {roleName} with ARN {roleArn}"); - - // Attach policy to allow SNS publishing - var policyDocument = $@"{{ - ""Version"": ""2012-10-17"", - ""Statement"": [ - {{ - ""Effect"": ""Allow"", - ""Action"": ""sns:Publish"", - ""Resource"": ""{snsTopicArn}"" - }} - ] - }}"; - - var putRolePolicyRequest = new Amazon.IdentityManagement.Model.PutRolePolicyRequest - { - RoleName = roleName, - PolicyName = "IoTSNSPolicy", - PolicyDocument = policyDocument - }; + var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + + if (describeStacksResponse.Stacks.Count > 0) + { + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_COMPLETE) + { + return true; + } + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_FAILED || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + return false; + } + } - await _amazonIAM.PutRolePolicyAsync(putRolePolicyRequest); - _logger.LogInformation($"Attached SNS policy to role {roleName}"); + Console.WriteLine("Waiting for CloudFormation stack creation to complete..."); + await Task.Delay(retryDelay); + retryCount++; + } - // Wait a bit for the role to propagate - await Task.Delay(10000); + _logger.LogError("Timed out waiting for CloudFormation stack creation to complete."); + return false; + } + + /// + /// Gets the outputs from the CloudFormation stack. + /// + /// The name of the CloudFormation stack. + /// A dictionary of stack outputs. + private static async Task?> GetStackOutputs(string stackName) + { + try + { + var describeStacksRequest = new DescribeStacksRequest + { + StackName = stackName + }; - return (snsTopicArn, roleArn); + var response = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + + if (response.Stacks.Count > 0) + { + var outputs = new Dictionary(); + foreach (var output in response.Stacks[0].Outputs) + { + outputs[output.OutputKey] = output.OutputValue; + } + return outputs; + } + + return null; } catch (Exception ex) { - _logger.LogError($"Couldn't set up resources. Here's why: {ex.Message}"); + _logger.LogError(ex, $"Failed to get stack outputs for {stackName}"); return null; } } - // snippet-end:[iot.dotnetv4.Setup] - // snippet-start:[iot.dotnetv4.Cleanup] /// - /// Cleans up the resources created during setup (SNS topic and IAM role). + /// Deletes the CloudFormation stack and waits for confirmation. /// - /// The ARN of the SNS topic to delete. - /// The name of the IAM role to delete. - /// True if cleanup was successful, false otherwise. - private static async Task CleanupAsync(string snsTopicArn, string roleName) + private static async Task DeleteCloudFormationStack(string stackName) { - var success = true; - try { - // Delete role policy first - try + var request = new DeleteStackRequest { - var deleteRolePolicyRequest = new Amazon.IdentityManagement.Model.DeleteRolePolicyRequest - { - RoleName = roleName, - PolicyName = "IoTSNSPolicy" - }; + StackName = stackName + }; + + await _amazonCloudFormation.DeleteStackAsync(request); + Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); + + bool stackDeleted = await WaitForStackDeletion(stackName); - await _amazonIAM.DeleteRolePolicyAsync(deleteRolePolicyRequest); - _logger.LogInformation($"Deleted role policy for {roleName}"); + if (stackDeleted) + { + Console.WriteLine($"CloudFormation stack '{stackName}' has been deleted."); + return true; } - catch (Exception ex) + else { - _logger.LogWarning($"Failed to delete role policy: {ex.Message}"); - success = false; + _logger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); + return false; } + } + catch (Exception ex) + { + _logger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); + return false; + } + } - // Delete IAM role - try - { - var deleteRoleRequest = new Amazon.IdentityManagement.Model.DeleteRoleRequest - { - RoleName = roleName - }; + /// + /// Waits for the stack to be deleted. + /// + private static async Task WaitForStackDeletion(string stackName) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; - await _amazonIAM.DeleteRoleAsync(deleteRoleRequest); - _logger.LogInformation($"Deleted IAM role {roleName}"); - } - catch (Exception ex) + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest { - _logger.LogWarning($"Failed to delete IAM role: {ex.Message}"); - success = false; - } + StackName = stackName + }; - // Delete SNS topic try { - var deleteTopicRequest = new Amazon.SimpleNotificationService.Model.DeleteTopicRequest - { - TopicArn = snsTopicArn - }; + var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); - await _amazonSNS.DeleteTopicAsync(deleteTopicRequest); - _logger.LogInformation($"Deleted SNS topic {snsTopicArn}"); + if (describeStacksResponse.Stacks.Count == 0 || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) + { + return true; + } } - catch (Exception ex) + catch (AmazonCloudFormationException ex) when (ex.ErrorCode == "ValidationError") { - _logger.LogWarning($"Failed to delete SNS topic: {ex.Message}"); - success = false; + return true; } - return success; + Console.WriteLine($"Waiting for CloudFormation stack '{stackName}' to be deleted..."); + await Task.Delay(retryDelay); + retryCount++; } - catch (Exception ex) + + _logger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); + return false; + } + + /// + /// Helper method to get a yes or no response from the user. + /// + private static bool GetYesNoResponse(string question) + { + Console.WriteLine(question); + var ynResponse = Console.ReadLine(); + var response = ynResponse != null && ynResponse.Equals("y", StringComparison.InvariantCultureIgnoreCase); + return response; + } + + /// + /// Prompts the user for a stack name. + /// + private static string PromptUserForStackName() + { + if (IsInteractive) { - _logger.LogError($"Couldn't clean up resources. Here's why: {ex.Message}"); - return false; + Console.Write($"Enter a name for the CloudFormation stack (press Enter for default '{_stackName}'): "); + string? input = Console.ReadLine(); + if (!string.IsNullOrWhiteSpace(input)) + { + var regex = new System.Text.RegularExpressions.Regex("[a-zA-Z][-a-zA-Z0-9]*"); + if (!regex.IsMatch(input)) + { + Console.WriteLine($"Invalid stack name. Using default: {_stackName}"); + return _stackName; + } + return input; + } } + return _stackName; } - // snippet-end:[iot.dotnetv4.Cleanup] } // snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index 41a0573fb20..16c59301e29 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -12,6 +12,7 @@ + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index f5c4728218f..aea78aeb5b8 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -4,7 +4,6 @@ using Amazon.IoT; using Amazon.IotData; using IoTActions; -using IoTScenarios; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -51,7 +50,7 @@ public IoTIntegrationTests(ITestOutputHelper output) public async Task IoTWrapperMethodsTest() { // Set to non-interactive mode for testing - IoTScenarios.IoTBasics.IsInteractive = false; + IoTBasics.IoTBasics.IsInteractive = false; var thingName = $"test-thing-{Guid.NewGuid():N}"; var certificateArn = ""; diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index bc7aaa6efe0..09399bd280f 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,12 +6,18 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program should create and manage these AWS resources automatically: +This program should create and manage these AWS resources automatically using CloudFormation: -1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. This role must be automatically created during the scenario execution with proper permissions to publish to SNS topics. -2. **snsAction** - An ARN of an SNS topic. This topic must be automatically created during the scenario execution for use with IoT rules. +1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IoT. This role is created through CloudFormation stack deployment with proper permissions to publish to SNS topics. +2. **snsAction** - An ARN of an SNS topic. This topic is created through CloudFormation stack deployment for use with IoT rules. -Both resources must be created during scenario setup and automatically cleaned up at the end of the scenario execution. +### CloudFormation Integration +- **Setup**: The scenario deploys a CloudFormation stack using the template file `iot_usecase/resources/cfn_template.yaml` +- **Resource Creation**: All required resources (SNS topic and IAM role) are defined in the CloudFormation template +- **Output Retrieval**: The scenario retrieves the SNS topic ARN and IAM role ARN from the CloudFormation stack outputs +- **Cleanup**: At the end of the scenario execution, the entire CloudFormation stack is deleted, ensuring all resources are properly cleaned up + +The CloudFormation template provides Infrastructure as Code (IaC) benefits, ensuring consistent and repeatable resource deployment across different environments. ## Hello AWS IoT From f7cf9a96148259c579101e8655ba3fcf9556cbfb Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:40:30 -0600 Subject: [PATCH 06/26] Fix search setup. --- dotnetv4/IoT/Actions/IoTWrapper.cs | 92 ++++++++++++++++++++++++--- scenarios/basics/iot/SPECIFICATION.md | 9 ++- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 738cd8ac985..ae9290a475f 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -384,15 +384,7 @@ public async Task> SearchIndexAsync(string queryString) { try { - await _amazonIoT.UpdateIndexingConfigurationAsync( - new UpdateIndexingConfigurationRequest() - { - ThingIndexingConfiguration = new ThingIndexingConfiguration() - { - ThingIndexingMode = ThingIndexingMode.REGISTRY - } - }); - + // First, try to perform the search var request = new SearchIndexRequest { QueryString = queryString @@ -402,6 +394,16 @@ await _amazonIoT.UpdateIndexingConfigurationAsync( _logger.LogInformation($"Search found {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.IndexNotReadyException ex) + { + _logger.LogWarning($"Search index not ready, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + _logger.LogWarning($"Search index not configured, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } catch (Amazon.IoT.Model.ThrottlingException ex) { _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); @@ -413,6 +415,78 @@ await _amazonIoT.UpdateIndexingConfigurationAsync( return new List(); } } + + /// + /// Sets up the indexing configuration and retries the search after waiting for the index to be ready. + /// + /// The search query string. + /// List of Things that match the search criteria, or empty list if setup/search failed. + private async Task> SetupIndexAndRetrySearchAsync(string queryString) + { + try + { + // Update indexing configuration to REGISTRY mode + _logger.LogInformation("Setting up IoT search indexing configuration..."); + await _amazonIoT.UpdateIndexingConfigurationAsync( + new UpdateIndexingConfigurationRequest() + { + ThingIndexingConfiguration = new ThingIndexingConfiguration() + { + ThingIndexingMode = ThingIndexingMode.REGISTRY + } + }); + + _logger.LogInformation("Indexing configuration updated. Waiting for index to be ready..."); + + // Wait for the index to be set up - this can take some time + const int maxRetries = 10; + const int retryDelaySeconds = 10; + + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + _logger.LogInformation($"Waiting for index to be ready (attempt {attempt}/{maxRetries})..."); + await Task.Delay(TimeSpan.FromSeconds(retryDelaySeconds)); + + // Try to get the current indexing configuration to see if it's ready + var configResponse = await _amazonIoT.GetIndexingConfigurationAsync(new GetIndexingConfigurationRequest()); + if (configResponse.ThingIndexingConfiguration?.ThingIndexingMode == ThingIndexingMode.REGISTRY) + { + // Try the search again + var request = new SearchIndexRequest + { + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things after index setup"); + return response.Things; + } + } + catch (Amazon.IoT.Model.IndexNotReadyException) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + catch (Amazon.IoT.Model.InvalidRequestException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + } + + _logger.LogWarning("Timeout waiting for search index to be ready after configuration update"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't set up search index configuration. Here's why: {ex.Message}"); + return new List(); + } + } // snippet-end:[iot.dotnetv4.SearchIndex] // snippet-start:[iot.dotnetv4.DetachThingPrincipal] diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 09399bd280f..6ec42575059 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -69,7 +69,14 @@ This scenario demonstrates the following key AWS IoT Service operations: - Use the `ListTopicRules` API to retrieve a list of all AWS IoT Rules. 12. **Search AWS IoT Things**: - - Use the `SearchThings` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. + - Use the `SearchIndex` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. + - **Automatic Index Configuration**: The search functionality includes intelligent handling of index setup: + - If the search index is not configured, the system automatically detects this condition through exception handling + - Catches `IndexNotReadyException` and `InvalidRequestException` that indicate the search index needs to be set up + - Automatically configures the Thing indexing mode to `REGISTRY` to enable search functionality + - Implements a retry mechanism with up to 10 attempts, waiting 10 seconds between each attempt for the index to become ready + - Validates the indexing configuration status before retrying search operations + - Provides detailed logging throughout the index setup process to keep users informed of progress 13. **Delete an AWS IoT Thing**: - Use the `DeleteThing` API to delete an AWS IoT Thing. From e3ac6f664895e36fdf780c3e58dd56d4e3116d43 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:06:03 -0600 Subject: [PATCH 07/26] Updates to integration tests. --- dotnetv4/IoT/Scenarios/IoTBasics.cs | 147 ++++++++++------- dotnetv4/IoT/Scenarios/appsettings.json | 11 -- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 186 +++++----------------- dotnetv4/IoT/Tests/IoTTests.csproj | 1 + 4 files changed, 132 insertions(+), 213 deletions(-) delete mode 100644 dotnetv4/IoT/Scenarios/appsettings.json diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 2aa59af617a..305945e7138 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -23,6 +23,9 @@ namespace IoTBasics; public class IoTBasics { public static bool IsInteractive = true; + public static IoTWrapper? Wrapper = null; + public static IAmazonCloudFormation? CloudFormationClient = null; + public static ILogger logger = null!; private static IoTWrapper _iotWrapper = null!; private static IAmazonCloudFormation _amazonCloudFormation = null!; private static ILogger _logger = null!; @@ -37,10 +40,6 @@ public class IoTBasics /// A Task object. public static async Task Main(string[] args) { - //var config = new ConfigurationBuilder() - // .AddJsonFile("appsettings.json") - // .Build(); - // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => @@ -61,13 +60,16 @@ public static async Task Main(string[] args) ) .Build(); - - - _logger = LoggerFactory.Create(builder => builder.AddConsole()) + logger = LoggerFactory.Create(builder => builder.AddConsole()) .CreateLogger(); - _iotWrapper = host.Services.GetRequiredService(); - _amazonCloudFormation = host.Services.GetRequiredService(); + Wrapper = host.Services.GetRequiredService(); + CloudFormationClient = host.Services.GetRequiredService(); + + // Set the private fields for backwards compatibility + _logger = logger; + _iotWrapper = Wrapper; + _amazonCloudFormation = CloudFormationClient; Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); @@ -99,7 +101,24 @@ public static async Task Main(string[] args) /// Run the IoT Basics scenario. /// /// A Task object. - private static async Task RunScenarioAsync() + public static async Task RunScenarioAsync() + { + // Use static properties if available, otherwise use private fields + var iotWrapper = Wrapper ?? _iotWrapper; + var cloudFormationClient = CloudFormationClient ?? _amazonCloudFormation; + var scenarioLogger = logger ?? _logger; + + await RunScenarioInternalAsync(iotWrapper, cloudFormationClient, scenarioLogger); + } + + /// + /// Internal method to run the IoT Basics scenario with injected dependencies. + /// + /// The IoT wrapper instance. + /// The CloudFormation client instance. + /// The logger instance. + /// A Task object. + private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { string thingName = $"iot-thing-{Guid.NewGuid():N}"; string certificateArn = ""; @@ -127,7 +146,7 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Using default Thing name: {thingName}"); } - var thingArn = await _iotWrapper.CreateThingAsync(thingName); + var thingArn = await iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); Console.WriteLine(new string('-', 80)); @@ -150,7 +169,7 @@ private static async Task RunScenarioAsync() if (createCert?.ToLower() == "y") { - var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + var certificateResult = await iotWrapper.CreateKeysAndCertificateAsync(); if (certificateResult.HasValue) { var (certArn, certPem, certId) = certificateResult.Value; @@ -174,7 +193,7 @@ private static async Task RunScenarioAsync() // Step 3: Attach the Certificate to the AWS IoT Thing Console.WriteLine("Attach the certificate to the AWS IoT Thing."); - var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + var attachResult = await iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); if (attachResult) { Console.WriteLine("Certificate attached to Thing successfully."); @@ -214,7 +233,7 @@ private static async Task RunScenarioAsync() { "Firmware", "1.2.3" } }; - await _iotWrapper.UpdateThingAsync(thingName, attributes); + await iotWrapper.UpdateThingAsync(thingName, attributes); Console.WriteLine("Thing attributes updated successfully."); Console.WriteLine(new string('-', 80)); @@ -229,7 +248,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var endpoint = await _iotWrapper.DescribeEndpointAsync(); + var endpoint = await iotWrapper.DescribeEndpointAsync(); if (endpoint != null) { var subdomain = endpoint.Split('.')[0]; @@ -251,7 +270,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var certificates = await _iotWrapper.ListCertificatesAsync(); + var certificates = await iotWrapper.ListCertificatesAsync(); foreach (var cert in certificates.Take(5)) // Show first 5 certificates { Console.WriteLine($"Cert id: {cert.CertificateId}"); @@ -285,7 +304,7 @@ private static async Task RunScenarioAsync() } }); - await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + await iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); Console.WriteLine("Thing Shadow updated successfully."); Console.WriteLine(new string('-', 80)); @@ -298,7 +317,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + var shadowData = await iotWrapper.GetThingShadowAsync(thingName); Console.WriteLine($"Received Shadow Data: {shadowData}"); Console.WriteLine(new string('-', 80)); @@ -329,12 +348,12 @@ private static async Task RunScenarioAsync() { _stackName = PromptUserForStackName(); - var deploySuccess = await DeployCloudFormationStack(_stackName); + var deploySuccess = await DeployCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); if (deploySuccess) { // Get stack outputs - var stackOutputs = await GetStackOutputs(_stackName); + var stackOutputs = await GetStackOutputs(_stackName, cloudFormationClient, scenarioLogger); if (stackOutputs != null) { snsTopicArn = stackOutputs["SNSTopicArn"]; @@ -344,7 +363,7 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); // Now create the IoT rule with the CloudFormation outputs - var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); if (ruleResult) { Console.WriteLine("IoT Rule created successfully."); @@ -379,7 +398,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var rules = await _iotWrapper.ListTopicRulesAsync(); + var rules = await iotWrapper.ListTopicRulesAsync(); Console.WriteLine("List of IoT Rules:"); foreach (var rule in rules.Take(5)) // Show first 5 rules { @@ -399,7 +418,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + var searchResults = await iotWrapper.SearchIndexAsync($"thingName:{thingName}"); if (searchResults.Any()) { Console.WriteLine($"Thing id found using search is {searchResults.First().ThingId}"); @@ -434,10 +453,10 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); - await _iotWrapper.DeleteCertificateAsync(certificateId); + await iotWrapper.DeleteCertificateAsync(certificateId); Console.WriteLine($"{certificateArn} was successfully deleted."); } Console.WriteLine(new string('-', 80)); @@ -459,7 +478,7 @@ private static async Task RunScenarioAsync() if (deleteThing?.ToLower() == "y") { - await _iotWrapper.DeleteThingAsync(thingName); + await iotWrapper.DeleteThingAsync(thingName); Console.WriteLine($"Deleted Thing {thingName}"); } Console.WriteLine(new string('-', 80)); @@ -474,7 +493,7 @@ private static async Task RunScenarioAsync() var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { - var cleanupSuccess = await DeleteCloudFormationStack(_stackName); + var cleanupSuccess = await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); if (cleanupSuccess) { Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); @@ -493,19 +512,19 @@ private static async Task RunScenarioAsync() } catch (Exception ex) { - _logger.LogError(ex, "Error occurred during scenario execution."); + scenarioLogger.LogError(ex, "Error occurred during scenario execution."); // Cleanup on error if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) { try { - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); - await _iotWrapper.DeleteCertificateAsync(certificateId); + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await iotWrapper.DeleteCertificateAsync(certificateId); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during cleanup."); } } @@ -513,11 +532,11 @@ private static async Task RunScenarioAsync() { try { - await _iotWrapper.DeleteThingAsync(thingName); + await iotWrapper.DeleteThingAsync(thingName); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during Thing cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during Thing cleanup."); } } @@ -526,11 +545,11 @@ private static async Task RunScenarioAsync() { try { - await DeleteCloudFormationStack(_stackName); + await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } @@ -542,8 +561,10 @@ private static async Task RunScenarioAsync() /// Deploys the CloudFormation stack with the necessary resources. /// /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// True if the stack was deployed successfully. - private static async Task DeployCloudFormationStack(string stackName) + private static async Task DeployCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); @@ -556,13 +577,13 @@ private static async Task DeployCloudFormationStack(string stackName) Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } }; - var response = await _amazonCloudFormation.CreateStackAsync(request); + var response = await cloudFormationClient.CreateStackAsync(request); if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) { Console.WriteLine($"CloudFormation stack creation started: {stackName}"); - bool stackCreated = await WaitForStackCompletion(response.StackId); + bool stackCreated = await WaitForStackCompletion(response.StackId, cloudFormationClient, scenarioLogger); if (stackCreated) { @@ -571,25 +592,25 @@ private static async Task DeployCloudFormationStack(string stackName) } else { - _logger.LogError($"CloudFormation stack creation failed: {stackName}"); + scenarioLogger.LogError($"CloudFormation stack creation failed: {stackName}"); return false; } } else { - _logger.LogError($"Failed to create CloudFormation stack: {stackName}"); + scenarioLogger.LogError($"Failed to create CloudFormation stack: {stackName}"); return false; } } catch (AlreadyExistsException) { - _logger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); + scenarioLogger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); var newStackName = PromptUserForStackName(); - return await DeployCloudFormationStack(newStackName); + return await DeployCloudFormationStack(newStackName, cloudFormationClient, scenarioLogger); } catch (Exception ex) { - _logger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); + scenarioLogger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); return false; } } @@ -598,8 +619,10 @@ private static async Task DeployCloudFormationStack(string stackName) /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. /// /// The ID of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// True if the stack was created successfully. - private static async Task WaitForStackCompletion(string stackId) + private static async Task WaitForStackCompletion(string stackId, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { int retryCount = 0; const int maxRetries = 30; @@ -612,7 +635,7 @@ private static async Task WaitForStackCompletion(string stackId) StackName = stackId }; - var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count > 0) { @@ -632,7 +655,7 @@ private static async Task WaitForStackCompletion(string stackId) retryCount++; } - _logger.LogError("Timed out waiting for CloudFormation stack creation to complete."); + scenarioLogger.LogError("Timed out waiting for CloudFormation stack creation to complete."); return false; } @@ -640,8 +663,10 @@ private static async Task WaitForStackCompletion(string stackId) /// Gets the outputs from the CloudFormation stack. /// /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// A dictionary of stack outputs. - private static async Task?> GetStackOutputs(string stackName) + private static async Task?> GetStackOutputs(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { try { @@ -650,7 +675,7 @@ private static async Task WaitForStackCompletion(string stackId) StackName = stackName }; - var response = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var response = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (response.Stacks.Count > 0) { @@ -666,7 +691,7 @@ private static async Task WaitForStackCompletion(string stackId) } catch (Exception ex) { - _logger.LogError(ex, $"Failed to get stack outputs for {stackName}"); + scenarioLogger.LogError(ex, $"Failed to get stack outputs for {stackName}"); return null; } } @@ -674,7 +699,11 @@ private static async Task WaitForStackCompletion(string stackId) /// /// Deletes the CloudFormation stack and waits for confirmation. /// - private static async Task DeleteCloudFormationStack(string stackName) + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task DeleteCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { try { @@ -683,10 +712,10 @@ private static async Task DeleteCloudFormationStack(string stackName) StackName = stackName }; - await _amazonCloudFormation.DeleteStackAsync(request); + await cloudFormationClient.DeleteStackAsync(request); Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); - bool stackDeleted = await WaitForStackDeletion(stackName); + bool stackDeleted = await WaitForStackDeletion(stackName, cloudFormationClient, scenarioLogger); if (stackDeleted) { @@ -695,13 +724,13 @@ private static async Task DeleteCloudFormationStack(string stackName) } else { - _logger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); + scenarioLogger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); + scenarioLogger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); return false; } } @@ -709,7 +738,11 @@ private static async Task DeleteCloudFormationStack(string stackName) /// /// Waits for the stack to be deleted. /// - private static async Task WaitForStackDeletion(string stackName) + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task WaitForStackDeletion(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { int retryCount = 0; const int maxRetries = 30; @@ -724,7 +757,7 @@ private static async Task WaitForStackDeletion(string stackName) try { - var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count == 0 || describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) @@ -742,7 +775,7 @@ private static async Task WaitForStackDeletion(string stackName) retryCount++; } - _logger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); + scenarioLogger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); return false; } diff --git a/dotnetv4/IoT/Scenarios/appsettings.json b/dotnetv4/IoT/Scenarios/appsettings.json deleted file mode 100644 index a84bda6ba5b..00000000000 --- a/dotnetv4/IoT/Scenarios/appsettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "AwsIotDataConfig": { - "Profile": "default", - "ServiceURL": "https://data.iot.us-east-1.amazonaws.com/" - }, - - "AwsConfig": { - "Profile": "default", - "Region": "us-east-1" - } -} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index aea78aeb5b8..1a84da3773d 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -1,168 +1,64 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using Amazon.CloudFormation; using Amazon.IoT; using Amazon.IotData; using IoTActions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Moq; using Xunit; -using Xunit.Abstractions; namespace IoTTests; /// -/// Integration tests for the IoT wrapper methods. +/// Integration tests for the AWS IoT Basics scenario. /// -public class IoTIntegrationTests +public class IoTBasicsTests { - private readonly ITestOutputHelper _output; - private readonly IoTWrapper _iotWrapper; - /// - /// Constructor for the test class. + /// Verifies the scenario with an integration test. No errors should be logged. /// - /// ITestOutputHelper object. - public IoTIntegrationTests(ITestOutputHelper output) - { - _output = output; - - // Set up dependency injection for the Amazon service. - var host = Host.CreateDefaultBuilder() - .ConfigureServices((_, services) => - services.AddAWSService() - .AddAWSService() - .AddTransient() - .AddLogging(builder => builder.AddConsole()) - ) - .Build(); - - _iotWrapper = host.Services.GetRequiredService(); - } - - /// - /// Test the IoT wrapper methods by running through the scenario. - /// - /// A Task object. + /// Async task. [Fact] [Trait("Category", "Integration")] - public async Task IoTWrapperMethodsTest() + public async Task TestScenarioIntegration() { - // Set to non-interactive mode for testing + // Arrange IoTBasics.IoTBasics.IsInteractive = false; - - var thingName = $"test-thing-{Guid.NewGuid():N}"; - var certificateArn = ""; - var certificateId = ""; - - try - { - _output.WriteLine("Starting IoT integration test..."); - - // 1. Create an IoT Thing - _output.WriteLine($"Creating IoT Thing: {thingName}"); - var thingArn = await _iotWrapper.CreateThingAsync(thingName); - Assert.False(string.IsNullOrEmpty(thingArn)); - _output.WriteLine($"Created Thing with ARN: {thingArn}"); - - // 2. Create a certificate - _output.WriteLine("Creating device certificate..."); - var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); - Assert.True(certificateResult.HasValue); - var (certArn, certPem, certId) = certificateResult.Value; - certificateArn = certArn; - certificateId = certId; - Assert.False(string.IsNullOrEmpty(certificateArn)); - Assert.False(string.IsNullOrEmpty(certPem)); - Assert.False(string.IsNullOrEmpty(certificateId)); - _output.WriteLine($"Created certificate with ARN: {certificateArn}"); - - // 3. Attach certificate to Thing - _output.WriteLine("Attaching certificate to Thing..."); - var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); - Assert.True(attachResult); - - // 4. Update Thing with attributes - _output.WriteLine("Updating Thing attributes..."); - var attributes = new Dictionary - { - { "TestAttribute", "TestValue" }, - { "Environment", "Testing" } - }; - var updateResult = await _iotWrapper.UpdateThingAsync(thingName, attributes); - Assert.True(updateResult); - - // 5. Get IoT endpoint - _output.WriteLine("Getting IoT endpoint..."); - var endpoint = await _iotWrapper.DescribeEndpointAsync(); - Assert.False(string.IsNullOrEmpty(endpoint)); - _output.WriteLine($"Retrieved endpoint: {endpoint}"); - - // 6. List certificates - _output.WriteLine("Listing certificates..."); - var certificates = await _iotWrapper.ListCertificatesAsync(); - Assert.NotNull(certificates); - Assert.True(certificates.Count > 0); - _output.WriteLine($"Found {certificates.Count} certificates"); - - // 7. Update Thing shadow - _output.WriteLine("Updating Thing shadow..."); - var shadowPayload = """{"state": {"desired": {"temperature": 22, "humidity": 45}}}"""; - var shadowResult = await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); - Assert.True(shadowResult); - - // 8. Get Thing shadow - _output.WriteLine("Getting Thing shadow..."); - var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); - Assert.False(string.IsNullOrEmpty(shadowData)); - _output.WriteLine($"Retrieved shadow data: {shadowData}"); - - // 9. List topic rules - _output.WriteLine("Listing topic rules..."); - var rules = await _iotWrapper.ListTopicRulesAsync(); - Assert.NotNull(rules); - _output.WriteLine($"Found {rules.Count} IoT rules"); - - // 10. Search Things - _output.WriteLine("Searching for Things..."); - var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); - Assert.NotNull(searchResults); - // Note: Search may not immediately return results for newly created Things - _output.WriteLine($"Search returned {searchResults.Count} results"); - - // 11. List Things - _output.WriteLine("Listing Things..."); - var things = await _iotWrapper.ListThingsAsync(); - Assert.NotNull(things); - Assert.True(things.Count > 0); - _output.WriteLine($"Found {things.Count} Things"); - - _output.WriteLine("IoT integration test completed successfully!"); - } - finally - { - // Cleanup resources - try - { - if (!string.IsNullOrEmpty(certificateArn)) - { - _output.WriteLine("Cleaning up: Detaching certificate from Thing..."); - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); - - _output.WriteLine("Cleaning up: Deleting certificate..."); - await _iotWrapper.DeleteCertificateAsync(certificateId); - } - _output.WriteLine("Cleaning up: Deleting Thing..."); - await _iotWrapper.DeleteThingAsync(thingName); - - _output.WriteLine("Cleanup completed successfully."); - } - catch (Exception ex) - { - _output.WriteLine($"Warning: Cleanup failed: {ex.Message}"); - } - } + var loggerScenarioMock = new Mock>(); + var loggerWrapperMock = new Mock>(); + + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + // Act + IoTBasics.IoTBasics.logger = loggerScenarioMock.Object; + + // Set up the wrapper and CloudFormation client + IoTBasics.IoTBasics.Wrapper = new IoTWrapper( + new AmazonIoTClient(), + new AmazonIotDataClient("https://dummy-iot-endpoint.amazonaws.com/"), + loggerWrapperMock.Object); + + IoTBasics.IoTBasics.CloudFormationClient = new AmazonCloudFormationClient(); + + await IoTBasics.IoTBasics.RunScenarioAsync(); + + // Assert no errors logged + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); } } diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index ed1b6b951ca..5a9ad27e2d9 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -11,6 +11,7 @@ + From db13a624a2793e2a788a1c706bcaa938276e2800 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:09:41 -0600 Subject: [PATCH 08/26] Fix formatting and warnings --- dotnetv4/IoT/Actions/HelloIoT.cs | 4 +-- dotnetv4/IoT/Actions/IoTWrapper.cs | 10 +++---- dotnetv4/IoT/Scenarios/IoTBasics.cs | 34 +++++++++++------------ dotnetv4/IoT/Scenarios/IoTBasics.csproj | 2 +- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 2 +- dotnetv4/IoT/Tests/IoTTests.csproj | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index de16511f44f..603bb8ea969 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -42,7 +42,7 @@ public static async Task Main(string[] args) Console.WriteLine($" Thing ARN: {thing.ThingArn}"); Console.WriteLine($" Thing Type: {thing.ThingTypeName ?? "No type specified"}"); Console.WriteLine($" Version: {thing.Version}"); - + if (thing.Attributes?.Count > 0) { Console.WriteLine(" Attributes:"); @@ -72,4 +72,4 @@ public static async Task Main(string[] args) } } } -// snippet-end:[iot.dotnetv4.Hello] +// snippet-end:[iot.dotnetv4.Hello] \ No newline at end of file diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index ae9290a475f..7fbb34c06c2 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -209,7 +209,7 @@ public async Task> ListCertificatesAsync() { var request = new ListCertificatesRequest(); var response = await _amazonIoT.ListCertificatesAsync(request); - + _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); return response.Certificates; } @@ -278,7 +278,7 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa var response = await _amazonIotData.GetThingShadowAsync(request); using var reader = new StreamReader(response.Payload); var shadowData = await reader.ReadToEndAsync(); - + _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); return shadowData; } @@ -357,7 +357,7 @@ public async Task> ListTopicRulesAsync() { var request = new ListTopicRulesRequest(); var response = await _amazonIoT.ListTopicRulesAsync(request); - + _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); return response.Rules; } @@ -607,7 +607,7 @@ public async Task> ListThingsAsync() { var request = new ListThingsRequest(); var response = await _amazonIoT.ListThingsAsync(request); - + _logger.LogInformation($"Retrieved {response.Things.Count} Things"); return response.Things; } @@ -625,4 +625,4 @@ public async Task> ListThingsAsync() // snippet-end:[iot.dotnetv4.ListThings] } -// snippet-end:[iot.dotnetv4.IoTWrapper] +// snippet-end:[iot.dotnetv4.IoTWrapper] \ No newline at end of file diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 305945e7138..43f042c78c1 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -43,13 +43,13 @@ public static async Task Main(string[] args) // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => - services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) + services.AddAWSService(new AWSOptions() { Region = RegionEndpoint.USEast1 }) .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) .AddSingleton(sp => { - var iotService = sp.GetService(); + var iotService = sp.GetRequiredService(); var request = new DescribeEndpointRequest { EndpointType = "iot:Data-ATS" @@ -133,7 +133,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("1. Create an AWS IoT Thing."); Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); Console.WriteLine(); - + if (IsInteractive) { Console.Write("Enter Thing name: "); @@ -145,7 +145,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo { Console.WriteLine($"Using default Thing name: {thingName}"); } - + var thingArn = await iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); Console.WriteLine(new string('-', 80)); @@ -155,7 +155,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("2. Generate a device certificate."); Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); Console.WriteLine(); - + var createCert = "y"; if (IsInteractive) { @@ -327,7 +327,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("Creates a rule that is an administrator-level action."); Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); Console.WriteLine(); - + if (IsInteractive) { Console.Write("Enter Rule name: "); @@ -342,7 +342,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Deploy CloudFormation stack to create SNS topic and IAM role Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); - + var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); if (deployStack) { @@ -358,10 +358,10 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo { snsTopicArn = stackOutputs["SNSTopicArn"]; string roleArn = stackOutputs["RoleArn"]; - + Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); - + // Now create the IoT rule with the CloudFormation outputs var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); if (ruleResult) @@ -489,7 +489,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine(new string('-', 80)); Console.WriteLine("13. Clean up CloudFormation stack."); Console.WriteLine("Deleting the CloudFormation stack and all resources..."); - + var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { @@ -513,7 +513,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo catch (Exception ex) { scenarioLogger.LogError(ex, "Error occurred during scenario execution."); - + // Cleanup on error if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) { @@ -527,7 +527,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo scenarioLogger.LogError(cleanupEx, "Error during cleanup."); } } - + if (!string.IsNullOrEmpty(thingName)) { try @@ -552,7 +552,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo scenarioLogger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } - + throw; } } @@ -574,7 +574,7 @@ private static async Task DeployCloudFormationStack(string stackName, IAma { StackName = stackName, TemplateBody = await File.ReadAllTextAsync(_stackResourcePath), - Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } + Capabilities = new List { Capability.CAPABILITY_NAMED_IAM } }; var response = await cloudFormationClient.CreateStackAsync(request); @@ -676,7 +676,7 @@ private static async Task WaitForStackCompletion(string stackId, IAmazonCl }; var response = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); - + if (response.Stacks.Count > 0) { var outputs = new Dictionary(); @@ -686,7 +686,7 @@ private static async Task WaitForStackCompletion(string stackId, IAmazonCl } return outputs; } - + return null; } catch (Exception ex) @@ -813,4 +813,4 @@ private static string PromptUserForStackName() return _stackName; } } -// snippet-end:[iot.dotnetv4.IoTScenario] +// snippet-end:[iot.dotnetv4.IoTScenario] \ No newline at end of file diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index 16c59301e29..b5409fe8683 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -12,7 +12,7 @@ - + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index 1a84da3773d..2616e612bb7 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -61,4 +61,4 @@ public async Task TestScenarioIntegration() It.IsAny>()), Times.Never); } -} +} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index 5a9ad27e2d9..84cf3d4fb76 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -11,7 +11,7 @@ - + From 1ae1944f8d55b84b41505bf4b363767bbadf81f7 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:24:05 -0600 Subject: [PATCH 09/26] Metadata and readme. --- .doc_gen/metadata/iot_metadata.yaml | 115 ++++++++++++++++++++++++ dotnetv4/IoT/README.md | 131 ++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 dotnetv4/IoT/README.md diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 6339c137a6f..64d72610ffb 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -22,6 +22,14 @@ iot_Hello: - description: snippet_tags: - iot.java2.hello_iot.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.Hello C++: versions: - sdk_version: 1 @@ -55,6 +63,14 @@ iot_DescribeEndpoint: - description: snippet_tags: - iot.java2.describe.endpoint.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DescribeEndpoint Rust: versions: - sdk_version: 1 @@ -75,6 +91,14 @@ iot_DescribeEndpoint: iot: {DescribeEndpoint} iot_ListThings: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListThings Rust: versions: - sdk_version: 1 @@ -104,6 +128,14 @@ iot_ListCertificates: - description: snippet_tags: - iot.java2.list.certs.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListCertificates C++: versions: - sdk_version: 1 @@ -133,6 +165,14 @@ iot_CreateKeysAndCertificate: - description: snippet_tags: - iot.java2.create.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateKeysAndCertificate C++: versions: - sdk_version: 1 @@ -162,6 +202,14 @@ iot_DeleteCertificate: - description: snippet_tags: - iot.java2.delete.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteCertificate C++: versions: - sdk_version: 1 @@ -191,6 +239,14 @@ iot_SearchIndex: - description: snippet_tags: - iot.java2.search.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.SearchIndex C++: versions: - sdk_version: 1 @@ -232,6 +288,14 @@ iot_DeleteThing: - description: snippet_tags: - iot.java2.delete.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteThing C++: versions: - sdk_version: 1 @@ -290,6 +354,14 @@ iot_AttachThingPrincipal: - description: snippet_tags: - iot.java2.attach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.AttachThingPrincipal C++: versions: - sdk_version: 1 @@ -319,6 +391,14 @@ iot_DetachThingPrincipal: - description: snippet_tags: - iot.java2.detach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DetachThingPrincipal C++: versions: - sdk_version: 1 @@ -348,6 +428,14 @@ iot_UpdateThing: - description: snippet_tags: - iot.java2.update.shadow.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.UpdateThing C++: versions: - sdk_version: 1 @@ -377,6 +465,14 @@ iot_CreateTopicRule: - description: snippet_tags: - iot.java2.create.rule.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateTopicRule C++: versions: - sdk_version: 1 @@ -418,6 +514,14 @@ iot_CreateThing: - description: snippet_tags: - iot.java2.create.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateThing C++: versions: - sdk_version: 1 @@ -464,6 +568,17 @@ iot_Scenario: - description: A wrapper class for &IoT; SDK methods. snippet_tags: - iot.java2.scenario.actions.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: Run an interactive scenario demonstrating &IoT; features. + snippet_tags: + - iot.dotnetv4.IoTScenario + - description: A wrapper class for &IoT; SDK methods. + snippet_tags: + - iot.dotnetv4.IoTWrapper C++: versions: - sdk_version: 1 diff --git a/dotnetv4/IoT/README.md b/dotnetv4/IoT/README.md new file mode 100644 index 00000000000..182d485e154 --- /dev/null +++ b/dotnetv4/IoT/README.md @@ -0,0 +1,131 @@ +# Amazon IoT code examples for the SDK for .NET (v4) + +## Overview + +Shows how to use the AWS SDK for .NET (v4) to work with AWS IoT Core. + + + + +_AWS IoT Core is a managed cloud service that lets connected devices easily and securely interact with cloud applications and other devices._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + + + +### Get started + +- [Hello AWS IoT](Actions/HelloIoT.cs#L20) (`ListThings`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](Scenarios/IoTBasics.cs) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [AttachThingPrincipal](Actions/IoTWrapper.cs#L156) +- [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L84) +- [CreateThing](Actions/IoTWrapper.cs#L34) +- [CreateTopicRule](Actions/IoTWrapper.cs#L336) +- [DeleteCertificate](Actions/IoTWrapper.cs#L556) +- [DeleteThing](Actions/IoTWrapper.cs#L589) +- [DescribeEndpoint](Actions/IoTWrapper.cs#L213) +- [DetachThingPrincipal](Actions/IoTWrapper.cs#L526) +- [GetThingShadow](Actions/IoTWrapper.cs#L312) +- [ListCertificates](Actions/IoTWrapper.cs#L243) +- [ListThings](Actions/IoTWrapper.cs#L614) +- [ListTopicRules](Actions/IoTWrapper.cs#L373) +- [SearchIndex](Actions/IoTWrapper.cs#L402) +- [UpdateThing](Actions/IoTWrapper.cs#L119) +- [UpdateThingShadow](Actions/IoTWrapper.cs#L280) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS IoT + +This example shows you how to get started using AWS IoT Core. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint specific to the Amazon Web Services account. +- List your AWS IoT certificates. +- Create an AWS IoT shadow that refers to a digital representation or virtual twin of a physical IoT device. +- Write out the state information, in JSON format. +- Creates a rule that is an administrator-level action. +- List your rules. +- Search things using the Thing name. +- Clean up resources. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + + + + + +## Additional resources + +- [AWS IoT Core Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) +- [AWS IoT Core API Reference](https://docs.aws.amazon.com/iot/latest/apireference/Welcome.html) +- [SDK for .NET (v4) AWS IoT reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/IoT/NIoT.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From 4fdc494aaa38830da654dbacd69c75086402a8ad Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:34:20 -0600 Subject: [PATCH 10/26] Update SPECIFICATION.md --- scenarios/basics/iot/SPECIFICATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 6ec42575059..ad3ddf0ca41 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,7 +6,7 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program should create and manage these AWS resources automatically using CloudFormation: +This program should create and manage these AWS resources automatically using CloudFormation and the provided stack .yaml file: 1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IoT. This role is created through CloudFormation stack deployment with proper permissions to publish to SNS topics. 2. **snsAction** - An ARN of an SNS topic. This topic is created through CloudFormation stack deployment for use with IoT rules. From a74d145d0bbb03beecfa8d4e4a3e42e2a8da5045 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:29:23 -0600 Subject: [PATCH 11/26] Initial CodeLoom project and file creation. --- dotnetv4/IoT/Actions/HelloIoT.cs | 75 ++++ dotnetv4/IoT/Actions/IoTActions.csproj | 24 ++ dotnetv4/IoT/Actions/IoTWrapper.cs | 471 ++++++++++++++++++++++ dotnetv4/IoT/IoTExamples.sln | 36 ++ dotnetv4/IoT/Scenarios/IoTBasics.cs | 355 ++++++++++++++++ dotnetv4/IoT/Scenarios/IoTBasics.csproj | 28 ++ dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 163 ++++++++ dotnetv4/IoT/Tests/IoTTests.csproj | 34 ++ 8 files changed, 1186 insertions(+) create mode 100644 dotnetv4/IoT/Actions/HelloIoT.cs create mode 100644 dotnetv4/IoT/Actions/IoTActions.csproj create mode 100644 dotnetv4/IoT/Actions/IoTWrapper.cs create mode 100644 dotnetv4/IoT/IoTExamples.sln create mode 100644 dotnetv4/IoT/Scenarios/IoTBasics.cs create mode 100644 dotnetv4/IoT/Scenarios/IoTBasics.csproj create mode 100644 dotnetv4/IoT/Tests/IoTIntegrationTests.cs create mode 100644 dotnetv4/IoT/Tests/IoTTests.csproj diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs new file mode 100644 index 00000000000..60a61f9671f --- /dev/null +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.Hello] +/// +/// Hello AWS IoT example. +/// +public class HelloIoT +{ + /// + /// Main method to run the Hello IoT example. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + var iotClient = new AmazonIoTClient(); + + try + { + Console.WriteLine("Hello AWS IoT! Let's list your IoT Things:"); + Console.WriteLine(new string('-', 80)); + + var request = new ListThingsRequest + { + MaxResults = 10 + }; + + var response = await iotClient.ListThingsAsync(request); + + if (response.Things.Count > 0) + { + Console.WriteLine($"Found {response.Things.Count} IoT Things:"); + foreach (var thing in response.Things) + { + Console.WriteLine($"- Thing Name: {thing.ThingName}"); + Console.WriteLine($" Thing ARN: {thing.ThingArn}"); + Console.WriteLine($" Thing Type: {thing.ThingTypeName ?? "No type specified"}"); + Console.WriteLine($" Version: {thing.Version}"); + + if (thing.Attributes?.Count > 0) + { + Console.WriteLine(" Attributes:"); + foreach (var attr in thing.Attributes) + { + Console.WriteLine($" {attr.Key}: {attr.Value}"); + } + } + Console.WriteLine(); + } + } + else + { + Console.WriteLine("No IoT Things found in your account."); + Console.WriteLine("You can create IoT Things using the IoT Basics scenario example."); + } + + Console.WriteLine("Hello IoT completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + finally + { + iotClient.Dispose(); + } + } +} +// snippet-end:[iot.dotnetv4.Hello] diff --git a/dotnetv4/IoT/Actions/IoTActions.csproj b/dotnetv4/IoT/Actions/IoTActions.csproj new file mode 100644 index 00000000000..8a0ab2e6143 --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTActions.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs new file mode 100644 index 00000000000..e070dd27233 --- /dev/null +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -0,0 +1,471 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IoT.Model; +using Amazon.IotData; +using Amazon.IotData.Model; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace IoTActions; + +// snippet-start:[iot.dotnetv4.IoTWrapper] +/// +/// Wrapper methods to use Amazon IoT Core with .NET. +/// +public class IoTWrapper +{ + private readonly IAmazonIoT _amazonIoT; + private readonly IAmazonIotData _amazonIotData; + private readonly ILogger _logger; + + /// + /// Constructor for the IoT wrapper. + /// + /// The injected IoT client. + /// The injected IoT Data client. + /// The injected logger. + public IoTWrapper(IAmazonIoT amazonIoT, IAmazonIotData amazonIotData, ILogger logger) + { + _amazonIoT = amazonIoT; + _amazonIotData = amazonIotData; + _logger = logger; + } + + // snippet-start:[iot.dotnetv4.CreateThing] + /// + /// Creates an AWS IoT Thing. + /// + /// The name of the Thing to create. + /// The ARN of the Thing created. + public async Task CreateThingAsync(string thingName) + { + try + { + var request = new CreateThingRequest + { + ThingName = thingName + }; + + var response = await _amazonIoT.CreateThingAsync(request); + _logger.LogInformation($"Created Thing {thingName} with ARN {response.ThingArn}"); + return response.ThingArn; + } + catch (Exception ex) + { + _logger.LogError($"Error creating Thing {thingName}: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateThing] + + // snippet-start:[iot.dotnetv4.CreateKeysAndCertificate] + /// + /// Creates a device certificate for AWS IoT. + /// + /// The certificate details including ARN and certificate PEM. + public async Task<(string CertificateArn, string CertificatePem, string CertificateId)> CreateKeysAndCertificateAsync() + { + try + { + var request = new CreateKeysAndCertificateRequest + { + SetAsActive = true + }; + + var response = await _amazonIoT.CreateKeysAndCertificateAsync(request); + _logger.LogInformation($"Created certificate with ARN {response.CertificateArn}"); + return (response.CertificateArn, response.CertificatePem, response.CertificateId); + } + catch (Exception ex) + { + _logger.LogError($"Error creating certificate: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateKeysAndCertificate] + + // snippet-start:[iot.dotnetv4.AttachThingPrincipal] + /// + /// Attaches a certificate to an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to attach. + /// True if successful. + public async Task AttachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new AttachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.AttachThingPrincipalAsync(request); + _logger.LogInformation($"Attached certificate {certificateArn} to Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error attaching certificate to Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.AttachThingPrincipal] + + // snippet-start:[iot.dotnetv4.UpdateThing] + /// + /// Updates an IoT Thing with attributes. + /// + /// The name of the Thing to update. + /// Dictionary of attributes to add. + /// True if successful. + public async Task UpdateThingAsync(string thingName, Dictionary attributes) + { + try + { + var request = new UpdateThingRequest + { + ThingName = thingName, + AttributePayload = new AttributePayload + { + Attributes = attributes, + Merge = true + } + }; + + await _amazonIoT.UpdateThingAsync(request); + _logger.LogInformation($"Updated Thing {thingName} with attributes"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error updating Thing attributes: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.UpdateThing] + + // snippet-start:[iot.dotnetv4.DescribeEndpoint] + /// + /// Gets the AWS IoT endpoint URL. + /// + /// The endpoint URL. + public async Task DescribeEndpointAsync() + { + try + { + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + + var response = await _amazonIoT.DescribeEndpointAsync(request); + _logger.LogInformation($"Retrieved endpoint: {response.EndpointAddress}"); + return response.EndpointAddress; + } + catch (Exception ex) + { + _logger.LogError($"Error describing endpoint: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DescribeEndpoint] + + // snippet-start:[iot.dotnetv4.ListCertificates] + /// + /// Lists all certificates associated with the account. + /// + /// List of certificate information. + public async Task> ListCertificatesAsync() + { + try + { + var request = new ListCertificatesRequest(); + var response = await _amazonIoT.ListCertificatesAsync(request); + + _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); + return response.Certificates; + } + catch (Exception ex) + { + _logger.LogError($"Error listing certificates: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListCertificates] + + // snippet-start:[iot.dotnetv4.UpdateThingShadow] + /// + /// Updates the Thing's shadow with new state information. + /// + /// The name of the Thing. + /// The shadow payload in JSON format. + /// True if successful. + public async Task UpdateThingShadowAsync(string thingName, string shadowPayload) + { + try + { + var request = new UpdateThingShadowRequest + { + ThingName = thingName, + Payload = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(shadowPayload)) + }; + + await _amazonIotData.UpdateThingShadowAsync(request); + _logger.LogInformation($"Updated shadow for Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error updating Thing shadow: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.UpdateThingShadow] + + // snippet-start:[iot.dotnetv4.GetThingShadow] + /// + /// Gets the Thing's shadow information. + /// + /// The name of the Thing. + /// The shadow data as a string. + public async Task GetThingShadowAsync(string thingName) + { + try + { + var request = new GetThingShadowRequest + { + ThingName = thingName + }; + + var response = await _amazonIotData.GetThingShadowAsync(request); + using var reader = new StreamReader(response.Payload); + var shadowData = await reader.ReadToEndAsync(); + + _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); + return shadowData; + } + catch (Exception ex) + { + _logger.LogError($"Error getting Thing shadow: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.GetThingShadow] + + // snippet-start:[iot.dotnetv4.CreateTopicRule] + /// + /// Creates an IoT topic rule. + /// + /// The name of the rule. + /// The ARN of the SNS topic for the action. + /// The ARN of the IAM role. + /// True if successful. + public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn, string roleArn) + { + try + { + var request = new CreateTopicRuleRequest + { + RuleName = ruleName, + TopicRulePayload = new TopicRulePayload + { + Sql = "SELECT * FROM 'topic/subtopic'", + Description = $"Rule created by .NET example: {ruleName}", + Actions = new List + { + new Amazon.IoT.Model.Action + { + Sns = new SnsAction + { + TargetArn = snsTopicArn, + RoleArn = roleArn + } + } + }, + RuleDisabled = false + } + }; + + await _amazonIoT.CreateTopicRuleAsync(request); + _logger.LogInformation($"Created IoT rule {ruleName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error creating topic rule: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.CreateTopicRule] + + // snippet-start:[iot.dotnetv4.ListTopicRules] + /// + /// Lists all IoT topic rules. + /// + /// List of topic rules. + public async Task> ListTopicRulesAsync() + { + try + { + var request = new ListTopicRulesRequest(); + var response = await _amazonIoT.ListTopicRulesAsync(request); + + _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); + return response.Rules; + } + catch (Exception ex) + { + _logger.LogError($"Error listing topic rules: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListTopicRules] + + // snippet-start:[iot.dotnetv4.SearchIndex] + /// + /// Searches for IoT Things using the search index. + /// + /// The search query string. + /// List of Things that match the search criteria. + public async Task> SearchIndexAsync(string queryString) + { + try + { + var request = new SearchIndexRequest + { + IndexName = "AWS_Things", + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things"); + return response.Things; + } + catch (Exception ex) + { + _logger.LogError($"Error searching index: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.SearchIndex] + + // snippet-start:[iot.dotnetv4.DetachThingPrincipal] + /// + /// Detaches a certificate from an IoT Thing. + /// + /// The name of the Thing. + /// The ARN of the certificate to detach. + /// True if successful. + public async Task DetachThingPrincipalAsync(string thingName, string certificateArn) + { + try + { + var request = new DetachThingPrincipalRequest + { + ThingName = thingName, + Principal = certificateArn + }; + + await _amazonIoT.DetachThingPrincipalAsync(request); + _logger.LogInformation($"Detached certificate {certificateArn} from Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error detaching certificate from Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DetachThingPrincipal] + + // snippet-start:[iot.dotnetv4.DeleteCertificate] + /// + /// Deletes an IoT certificate. + /// + /// The ID of the certificate to delete. + /// True if successful. + public async Task DeleteCertificateAsync(string certificateId) + { + try + { + // First, update the certificate to inactive state + var updateRequest = new UpdateCertificateRequest + { + CertificateId = certificateId, + NewStatus = CertificateStatus.INACTIVE + }; + await _amazonIoT.UpdateCertificateAsync(updateRequest); + + // Then delete the certificate + var deleteRequest = new DeleteCertificateRequest + { + CertificateId = certificateId + }; + + await _amazonIoT.DeleteCertificateAsync(deleteRequest); + _logger.LogInformation($"Deleted certificate {certificateId}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error deleting certificate: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DeleteCertificate] + + // snippet-start:[iot.dotnetv4.DeleteThing] + /// + /// Deletes an IoT Thing. + /// + /// The name of the Thing to delete. + /// True if successful. + public async Task DeleteThingAsync(string thingName) + { + try + { + var request = new DeleteThingRequest + { + ThingName = thingName + }; + + await _amazonIoT.DeleteThingAsync(request); + _logger.LogInformation($"Deleted Thing {thingName}"); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error deleting Thing: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.DeleteThing] + + // snippet-start:[iot.dotnetv4.ListThings] + /// + /// Lists IoT Things with pagination support. + /// + /// List of Things. + public async Task> ListThingsAsync() + { + try + { + var request = new ListThingsRequest(); + var response = await _amazonIoT.ListThingsAsync(request); + + _logger.LogInformation($"Retrieved {response.Things.Count} Things"); + return response.Things; + } + catch (Exception ex) + { + _logger.LogError($"Error listing Things: {ex.Message}"); + throw; + } + } + // snippet-end:[iot.dotnetv4.ListThings] +} +// snippet-end:[iot.dotnetv4.IoTWrapper] diff --git a/dotnetv4/IoT/IoTExamples.sln b/dotnetv4/IoT/IoTExamples.sln new file mode 100644 index 00000000000..812279b9465 --- /dev/null +++ b/dotnetv4/IoT/IoTExamples.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTActions", "Actions\IoTActions.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0001}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTBasics", "Scenarios\IoTBasics.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0002}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IoTTests", "Tests\IoTTests.csproj", "{A8F2F404-F1A3-4C0C-9478-2D99B95F0003}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0001}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0002}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8F2F404-F1A3-4C0C-9478-2D99B95F0003}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C73BCD70-F2E4-4A89-9E94-47E8C2B48A41} + EndGlobalSection +EndGlobal diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs new file mode 100644 index 00000000000..da7a2243b95 --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -0,0 +1,355 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IotData; +using Amazon.IoT.Model; +using IoTActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace IoTScenarios; + +// snippet-start:[iot.dotnetv4.IoTScenario] +/// +/// Scenario class for AWS IoT basics workflow. +/// +public class IoTBasics +{ + private static IoTWrapper _iotWrapper = null!; + private static ILogger _logger = null!; + + /// + /// Main method for the IoT Basics scenario. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + // Set up dependency injection for the Amazon service. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddAWSService() + .AddTransient() + .AddLogging(builder => builder.AddConsole()) + ) + .Build(); + + _logger = LoggerFactory.Create(builder => builder.AddConsole()) + .CreateLogger(); + + _iotWrapper = host.Services.GetRequiredService(); + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Welcome to the AWS IoT example workflow."); + Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); + Console.WriteLine("The program guides you through a series of steps, including creating an IoT Thing, generating a device certificate,"); + Console.WriteLine("updating the Thing with attributes, and so on. It utilizes the AWS SDK for .NET and incorporates functionalities"); + Console.WriteLine("for creating and managing IoT Things, certificates, rules, shadows, and performing searches."); + Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); + Console.WriteLine("developers working with AWS IoT in a .NET environment."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + Console.WriteLine(new string('-', 80)); + + try + { + await RunScenarioAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was a problem running the scenario."); + Console.WriteLine($"\nAn error occurred: {ex.Message}"); + } + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("The AWS IoT workflow has successfully completed."); + Console.WriteLine(new string('-', 80)); + } + + /// + /// Run the IoT Basics scenario. + /// + /// A Task object. + private static async Task RunScenarioAsync() + { + string thingName = ""; + string certificateArn = ""; + string certificateId = ""; + string ruleName = ""; + + try + { + // Step 1: Create an AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("1. Create an AWS IoT Thing."); + Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); + Console.WriteLine(); + Console.Write("Enter Thing name: "); + thingName = Console.ReadLine()!; + + var thingArn = await _iotWrapper.CreateThingAsync(thingName); + Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); + Console.WriteLine(new string('-', 80)); + + // Step 2: Generate a Device Certificate + Console.WriteLine(new string('-', 80)); + Console.WriteLine("2. Generate a device certificate."); + Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); + Console.WriteLine(); + Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); + var createCert = Console.ReadLine(); + + if (createCert?.ToLower() == "y") + { + var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + certificateArn = certArn; + certificateId = certId; + + Console.WriteLine($"\nCertificate:"); + // Show only first few lines of certificate for brevity + var lines = certPem.Split('\n'); + for (int i = 0; i < Math.Min(lines.Length, 5); i++) + { + Console.WriteLine(lines[i]); + } + if (lines.Length > 5) + { + Console.WriteLine("..."); + } + + Console.WriteLine($"\nCertificate ARN:"); + Console.WriteLine(certificateArn); + + // Step 3: Attach the Certificate to the AWS IoT Thing + Console.WriteLine("Attach the certificate to the AWS IoT Thing."); + await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + Console.WriteLine("Certificate attached to Thing successfully."); + + Console.WriteLine("Thing Details:"); + Console.WriteLine($"Thing Name: {thingName}"); + Console.WriteLine($"Thing ARN: {thingArn}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 4: Update an AWS IoT Thing with Attributes + Console.WriteLine(new string('-', 80)); + Console.WriteLine("3. Update an AWS IoT Thing with Attributes."); + Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); + Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var attributes = new Dictionary + { + { "Location", "Seattle" }, + { "DeviceType", "Sensor" }, + { "Firmware", "1.2.3" } + }; + + await _iotWrapper.UpdateThingAsync(thingName, attributes); + Console.WriteLine("Thing attributes updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 5: Return a unique endpoint specific to the Amazon Web Services account + Console.WriteLine(new string('-', 80)); + Console.WriteLine("4. Return a unique endpoint specific to the Amazon Web Services account."); + Console.WriteLine("An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var endpoint = await _iotWrapper.DescribeEndpointAsync(); + var subdomain = endpoint.Split('.')[0]; + Console.WriteLine($"Extracted subdomain: {subdomain}"); + Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + Console.WriteLine(new string('-', 80)); + + // Step 6: List your AWS IoT certificates + Console.WriteLine(new string('-', 80)); + Console.WriteLine("5. List your AWS IoT certificates"); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var certificates = await _iotWrapper.ListCertificatesAsync(); + foreach (var cert in certificates.Take(5)) // Show first 5 certificates + { + Console.WriteLine($"Cert id: {cert.CertificateId}"); + Console.WriteLine($"Cert Arn: {cert.CertificateArn}"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 7: Create an IoT shadow + Console.WriteLine(new string('-', 80)); + Console.WriteLine("6. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); + Console.WriteLine("A Thing Shadow refers to a feature that enables you to create a virtual representation, or \"shadow,\""); + Console.WriteLine("of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between"); + Console.WriteLine("the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow."); + Console.WriteLine(); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var shadowPayload = JsonSerializer.Serialize(new + { + state = new + { + desired = new + { + temperature = 25, + humidity = 50 + } + } + }); + + await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + Console.WriteLine("Thing Shadow updated successfully."); + Console.WriteLine(new string('-', 80)); + + // Step 8: Write out the state information, in JSON format + Console.WriteLine(new string('-', 80)); + Console.WriteLine("7. Write out the state information, in JSON format."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + Console.WriteLine($"Received Shadow Data: {shadowData}"); + Console.WriteLine(new string('-', 80)); + + // Step 9: Creates a rule + Console.WriteLine(new string('-', 80)); + Console.WriteLine("8. Creates a rule"); + Console.WriteLine("Creates a rule that is an administrator-level action."); + Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); + Console.WriteLine(); + Console.Write("Enter Rule name: "); + ruleName = Console.ReadLine()!; + + // Note: For demonstration, we'll use placeholder ARNs + // In real usage, these should be actual SNS topic and IAM role ARNs + var snsTopicArn = "arn:aws:sns:us-east-1:123456789012:example-topic"; + var roleArn = "arn:aws:iam::123456789012:role/IoTRole"; + + Console.WriteLine("Note: Using placeholder ARNs for SNS topic and IAM role."); + Console.WriteLine("In production, ensure these ARNs exist and have proper permissions."); + + try + { + await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + Console.WriteLine("IoT Rule created successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Note: Rule creation failed (expected with placeholder ARNs): {ex.Message}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 10: List your rules + Console.WriteLine(new string('-', 80)); + Console.WriteLine("9. List your rules."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var rules = await _iotWrapper.ListTopicRulesAsync(); + Console.WriteLine("List of IoT Rules:"); + foreach (var rule in rules.Take(5)) // Show first 5 rules + { + Console.WriteLine($"Rule Name: {rule.RuleName}"); + Console.WriteLine($"Rule ARN: {rule.RuleArn}"); + Console.WriteLine("--------------"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + + // Step 11: Search things using the Thing name + Console.WriteLine(new string('-', 80)); + Console.WriteLine("10. Search things using the Thing name."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + if (searchResults.Any()) + { + Console.WriteLine($"Thing id found using search is {searchResults.First().ThingId}"); + } + else + { + Console.WriteLine($"No search results found for Thing: {thingName}"); + } + Console.WriteLine(new string('-', 80)); + + // Step 12: Cleanup - Detach and delete certificate + if (!string.IsNullOrEmpty(certificateArn)) + { + Console.WriteLine(new string('-', 80)); + Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); + var deleteCert = Console.ReadLine(); + + if (deleteCert?.ToLower() == "y") + { + Console.WriteLine("11. You selected to detach and delete the certificate."); + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); + + await _iotWrapper.DeleteCertificateAsync(certificateId); + Console.WriteLine($"{certificateArn} was successfully deleted."); + } + Console.WriteLine(new string('-', 80)); + } + + // Step 13: Delete the AWS IoT Thing + Console.WriteLine(new string('-', 80)); + Console.WriteLine("12. Delete the AWS IoT Thing."); + Console.Write($"Do you want to delete the IoT Thing? (y/n)"); + var deleteThing = Console.ReadLine(); + + if (deleteThing?.ToLower() == "y") + { + await _iotWrapper.DeleteThingAsync(thingName); + Console.WriteLine($"Deleted Thing {thingName}"); + } + Console.WriteLine(new string('-', 80)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred during scenario execution."); + + // Cleanup on error + if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) + { + try + { + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await _iotWrapper.DeleteCertificateAsync(certificateId); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during cleanup."); + } + } + + if (!string.IsNullOrEmpty(thingName)) + { + try + { + await _iotWrapper.DeleteThingAsync(thingName); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during Thing cleanup."); + } + } + + throw; + } + } +} +// snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj new file mode 100644 index 00000000000..bc870c03ebf --- /dev/null +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs new file mode 100644 index 00000000000..1c52d1f813f --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -0,0 +1,163 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.IoT; +using Amazon.IotData; +using IoTActions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace IoTTests; + +/// +/// Integration tests for the IoT wrapper methods. +/// +public class IoTIntegrationTests +{ + private readonly ITestOutputHelper _output; + private readonly IoTWrapper _iotWrapper; + + /// + /// Constructor for the test class. + /// + /// ITestOutputHelper object. + public IoTIntegrationTests(ITestOutputHelper output) + { + _output = output; + + // Set up dependency injection for the Amazon service. + var host = Host.CreateDefaultBuilder() + .ConfigureServices((_, services) => + services.AddAWSService() + .AddAWSService() + .AddTransient() + .AddLogging(builder => builder.AddConsole()) + ) + .Build(); + + _iotWrapper = host.Services.GetRequiredService(); + } + + /// + /// Test the IoT wrapper methods by running through the scenario. + /// + /// A Task object. + [Fact] + [Trait("Category", "Integration")] + public async Task IoTWrapperMethodsTest() + { + var thingName = $"test-thing-{Guid.NewGuid():N}"; + var certificateArn = ""; + var certificateId = ""; + + try + { + _output.WriteLine("Starting IoT integration test..."); + + // 1. Create an IoT Thing + _output.WriteLine($"Creating IoT Thing: {thingName}"); + var thingArn = await _iotWrapper.CreateThingAsync(thingName); + Assert.False(string.IsNullOrEmpty(thingArn)); + _output.WriteLine($"Created Thing with ARN: {thingArn}"); + + // 2. Create a certificate + _output.WriteLine("Creating device certificate..."); + var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + certificateArn = certArn; + certificateId = certId; + Assert.False(string.IsNullOrEmpty(certificateArn)); + Assert.False(string.IsNullOrEmpty(certPem)); + Assert.False(string.IsNullOrEmpty(certificateId)); + _output.WriteLine($"Created certificate with ARN: {certificateArn}"); + + // 3. Attach certificate to Thing + _output.WriteLine("Attaching certificate to Thing..."); + var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + Assert.True(attachResult); + + // 4. Update Thing with attributes + _output.WriteLine("Updating Thing attributes..."); + var attributes = new Dictionary + { + { "TestAttribute", "TestValue" }, + { "Environment", "Testing" } + }; + var updateResult = await _iotWrapper.UpdateThingAsync(thingName, attributes); + Assert.True(updateResult); + + // 5. Get IoT endpoint + _output.WriteLine("Getting IoT endpoint..."); + var endpoint = await _iotWrapper.DescribeEndpointAsync(); + Assert.False(string.IsNullOrEmpty(endpoint)); + _output.WriteLine($"Retrieved endpoint: {endpoint}"); + + // 6. List certificates + _output.WriteLine("Listing certificates..."); + var certificates = await _iotWrapper.ListCertificatesAsync(); + Assert.NotNull(certificates); + Assert.True(certificates.Count > 0); + _output.WriteLine($"Found {certificates.Count} certificates"); + + // 7. Update Thing shadow + _output.WriteLine("Updating Thing shadow..."); + var shadowPayload = """{"state": {"desired": {"temperature": 22, "humidity": 45}}}"""; + var shadowResult = await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + Assert.True(shadowResult); + + // 8. Get Thing shadow + _output.WriteLine("Getting Thing shadow..."); + var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + Assert.False(string.IsNullOrEmpty(shadowData)); + _output.WriteLine($"Retrieved shadow data: {shadowData}"); + + // 9. List topic rules + _output.WriteLine("Listing topic rules..."); + var rules = await _iotWrapper.ListTopicRulesAsync(); + Assert.NotNull(rules); + _output.WriteLine($"Found {rules.Count} IoT rules"); + + // 10. Search Things + _output.WriteLine("Searching for Things..."); + var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + Assert.NotNull(searchResults); + // Note: Search may not immediately return results for newly created Things + _output.WriteLine($"Search returned {searchResults.Count} results"); + + // 11. List Things + _output.WriteLine("Listing Things..."); + var things = await _iotWrapper.ListThingsAsync(); + Assert.NotNull(things); + Assert.True(things.Count > 0); + _output.WriteLine($"Found {things.Count} Things"); + + _output.WriteLine("IoT integration test completed successfully!"); + } + finally + { + // Cleanup resources + try + { + if (!string.IsNullOrEmpty(certificateArn)) + { + _output.WriteLine("Cleaning up: Detaching certificate from Thing..."); + await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + + _output.WriteLine("Cleaning up: Deleting certificate..."); + await _iotWrapper.DeleteCertificateAsync(certificateId); + } + + _output.WriteLine("Cleaning up: Deleting Thing..."); + await _iotWrapper.DeleteThingAsync(thingName); + + _output.WriteLine("Cleanup completed successfully."); + } + catch (Exception ex) + { + _output.WriteLine($"Warning: Cleanup failed: {ex.Message}"); + } + } + } +} diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj new file mode 100644 index 00000000000..82596e09888 --- /dev/null +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + From 2c6055f9c907e4de4c41a1a0fe08610e6703e66c Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:20:07 -0600 Subject: [PATCH 12/26] Update specification. --- .../basics/controltower/SPECIFICATION.md | 2 +- scenarios/basics/iot/SPECIFICATION.md | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scenarios/basics/controltower/SPECIFICATION.md b/scenarios/basics/controltower/SPECIFICATION.md index b4e9c8590dc..58ee1fbfdc4 100644 --- a/scenarios/basics/controltower/SPECIFICATION.md +++ b/scenarios/basics/controltower/SPECIFICATION.md @@ -210,7 +210,7 @@ Thanks for watching! ## Errors The following errors are handled in the Control Tower wrapper class: -| action | Error | Handling | +| Action | Error | Handling | |------------------------|-----------------------|------------------------------------------------------------------------| | `ListBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | | `ListEnabledBaselines` | AccessDeniedException | Notify the user of insufficient permissions and exit. | diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 4b841154538..891e4123d60 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -68,6 +68,29 @@ This scenario demonstrates the following key AWS IoT Service operations: Note: We have buy off on these operations from IoT SME. +## Exception Handling + +Each AWS IoT operation can throw specific exceptions that should be handled appropriately. The following table lists the potential exceptions for each action: + +| Action | Error | Handling | +|------------------------|---------------------------------|------------------------------------------------------------------------| +| **CreateThing** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **CreateKeysAndCertificate** | ThrottlingException | Notify the user to try again later +| **AttachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **UpdateThing** | ResourceNotFoundException | Notify cannot perform action and return +| **DescribeEndpoint** | ThrottlingException | Notify the user to try again later +| **ListCertificates** | ThrottlingException | Notify the user to try again later +| **UpdateThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **GetThingShadow** | ResourceNotFoundException | Notify cannot perform action and return +| **CreateTopicRule** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **ListTopicRules** | ThrottlingException | Notify the user to try again later +| **SearchIndex** | ThrottlingException | Notify the user to try again later +| **DetachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteCertificate** | ResourceNotFoundException | Notify cannot perform action and return +| **DeleteThing** | ResourceNotFoundException | Notify cannot perform action and return +| **ListThings** | ThrottlingException | Notify the user to try again later + + ### Program execution This scenario does have user interaction. The following shows the output of the program. @@ -220,5 +243,5 @@ The following table describes the metadata used in this scenario. | `updateThing` | iot_metadata.yaml | iot_UpdateThing | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | -| `hello ` | iot_metadata.yaml | iot_Hello | +| `hello` | iot_metadata.yaml | iot_Hello | | `scenario | iot_metadata.yaml | iot_Scenario | From 4210f94dde2f8819c0aafbc13e4fe9d046ca1ff0 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:38:28 -0600 Subject: [PATCH 13/26] Update exception handling. --- dotnetv4/IoT/Actions/HelloIoT.cs | 2 +- dotnetv4/IoT/Actions/IoTWrapper.cs | 173 ++++++++++++++++------ dotnetv4/IoT/Scenarios/IoTBasics.cs | 74 +++++---- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 4 +- 4 files changed, 176 insertions(+), 77 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index 60a61f9671f..de16511f44f 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -33,7 +33,7 @@ public static async Task Main(string[] args) var response = await iotClient.ListThingsAsync(request); - if (response.Things.Count > 0) + if (response.Things is { Count: > 0 }) { Console.WriteLine($"Found {response.Things.Count} IoT Things:"); foreach (var thing in response.Things) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index e070dd27233..d0d61b0ed3f 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -38,8 +38,8 @@ public IoTWrapper(IAmazonIoT amazonIoT, IAmazonIotData amazonIotData, ILogger /// The name of the Thing to create. - /// The ARN of the Thing created. - public async Task CreateThingAsync(string thingName) + /// The ARN of the Thing created, or null if creation failed. + public async Task CreateThingAsync(string thingName) { try { @@ -52,10 +52,15 @@ public async Task CreateThingAsync(string thingName) _logger.LogInformation($"Created Thing {thingName} with ARN {response.ThingArn}"); return response.ThingArn; } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Thing {thingName} already exists: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error creating Thing {thingName}: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create Thing {thingName}. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.CreateThing] @@ -64,8 +69,8 @@ public async Task CreateThingAsync(string thingName) /// /// Creates a device certificate for AWS IoT. /// - /// The certificate details including ARN and certificate PEM. - public async Task<(string CertificateArn, string CertificatePem, string CertificateId)> CreateKeysAndCertificateAsync() + /// The certificate details including ARN and certificate PEM, or null if creation failed. + public async Task<(string CertificateArn, string CertificatePem, string CertificateId)?> CreateKeysAndCertificateAsync() { try { @@ -78,10 +83,15 @@ public async Task CreateThingAsync(string thingName) _logger.LogInformation($"Created certificate with ARN {response.CertificateArn}"); return (response.CertificateArn, response.CertificatePem, response.CertificateId); } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error creating certificate: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create certificate. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.CreateKeysAndCertificate] @@ -92,7 +102,7 @@ public async Task CreateThingAsync(string thingName) /// /// The name of the Thing. /// The ARN of the certificate to attach. - /// True if successful. + /// True if successful, false otherwise. public async Task AttachThingPrincipalAsync(string thingName, string certificateArn) { try @@ -107,10 +117,15 @@ public async Task AttachThingPrincipalAsync(string thingName, string certi _logger.LogInformation($"Attached certificate {certificateArn} to Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot attach certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error attaching certificate to Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't attach certificate to Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.AttachThingPrincipal] @@ -121,7 +136,7 @@ public async Task AttachThingPrincipalAsync(string thingName, string certi /// /// The name of the Thing to update. /// Dictionary of attributes to add. - /// True if successful. + /// True if successful, false otherwise. public async Task UpdateThingAsync(string thingName, Dictionary attributes) { try @@ -140,10 +155,15 @@ public async Task UpdateThingAsync(string thingName, Dictionary UpdateThingAsync(string thingName, Dictionary /// Gets the AWS IoT endpoint URL. /// - /// The endpoint URL. - public async Task DescribeEndpointAsync() + /// The endpoint URL, or null if retrieval failed. + public async Task DescribeEndpointAsync() { try { @@ -166,10 +186,15 @@ public async Task DescribeEndpointAsync() _logger.LogInformation($"Retrieved endpoint: {response.EndpointAddress}"); return response.EndpointAddress; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error describing endpoint: {ex.Message}"); - throw; + _logger.LogError($"Couldn't describe endpoint. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.DescribeEndpoint] @@ -178,7 +203,7 @@ public async Task DescribeEndpointAsync() /// /// Lists all certificates associated with the account. /// - /// List of certificate information. + /// List of certificate information, or empty list if listing failed. public async Task> ListCertificatesAsync() { try @@ -189,10 +214,15 @@ public async Task> ListCertificatesAsync() _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); return response.Certificates; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing certificates: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list certificates. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListCertificates] @@ -203,7 +233,7 @@ public async Task> ListCertificatesAsync() /// /// The name of the Thing. /// The shadow payload in JSON format. - /// True if successful. + /// True if successful, false otherwise. public async Task UpdateThingShadowAsync(string thingName, string shadowPayload) { try @@ -218,10 +248,15 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa _logger.LogInformation($"Updated shadow for Thing {thingName}"); return true; } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot update Thing shadow - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error updating Thing shadow: {ex.Message}"); - throw; + _logger.LogError($"Couldn't update Thing shadow. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.UpdateThingShadow] @@ -231,8 +266,8 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa /// Gets the Thing's shadow information. /// /// The name of the Thing. - /// The shadow data as a string. - public async Task GetThingShadowAsync(string thingName) + /// The shadow data as a string, or null if retrieval failed. + public async Task GetThingShadowAsync(string thingName) { try { @@ -248,10 +283,15 @@ public async Task GetThingShadowAsync(string thingName) _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); return shadowData; } + catch (Amazon.IotData.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot get Thing shadow - resource not found: {ex.Message}"); + return null; + } catch (Exception ex) { - _logger.LogError($"Error getting Thing shadow: {ex.Message}"); - throw; + _logger.LogError($"Couldn't get Thing shadow. Here's why: {ex.Message}"); + return null; } } // snippet-end:[iot.dotnetv4.GetThingShadow] @@ -263,7 +303,7 @@ public async Task GetThingShadowAsync(string thingName) /// The name of the rule. /// The ARN of the SNS topic for the action. /// The ARN of the IAM role. - /// True if successful. + /// True if successful, false otherwise. public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn, string roleArn) { try @@ -294,10 +334,15 @@ public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn _logger.LogInformation($"Created IoT rule {ruleName}"); return true; } + catch (Amazon.IoT.Model.ResourceAlreadyExistsException ex) + { + _logger.LogWarning($"Rule {ruleName} already exists: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error creating topic rule: {ex.Message}"); - throw; + _logger.LogError($"Couldn't create topic rule. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.CreateTopicRule] @@ -306,7 +351,7 @@ public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn /// /// Lists all IoT topic rules. /// - /// List of topic rules. + /// List of topic rules, or empty list if listing failed. public async Task> ListTopicRulesAsync() { try @@ -317,10 +362,15 @@ public async Task> ListTopicRulesAsync() _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); return response.Rules; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing topic rules: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list topic rules. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListTopicRules] @@ -330,7 +380,7 @@ public async Task> ListTopicRulesAsync() /// Searches for IoT Things using the search index. /// /// The search query string. - /// List of Things that match the search criteria. + /// List of Things that match the search criteria, or empty list if search failed. public async Task> SearchIndexAsync(string queryString) { try @@ -345,10 +395,15 @@ public async Task> SearchIndexAsync(string queryString) _logger.LogInformation($"Search found {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error searching index: {ex.Message}"); - throw; + _logger.LogError($"Couldn't search index. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.SearchIndex] @@ -359,7 +414,7 @@ public async Task> SearchIndexAsync(string queryString) /// /// The name of the Thing. /// The ARN of the certificate to detach. - /// True if successful. + /// True if successful, false otherwise. public async Task DetachThingPrincipalAsync(string thingName, string certificateArn) { try @@ -374,10 +429,15 @@ public async Task DetachThingPrincipalAsync(string thingName, string certi _logger.LogInformation($"Detached certificate {certificateArn} from Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot detach certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error detaching certificate from Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't detach certificate from Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DetachThingPrincipal] @@ -387,7 +447,7 @@ public async Task DetachThingPrincipalAsync(string thingName, string certi /// Deletes an IoT certificate. /// /// The ID of the certificate to delete. - /// True if successful. + /// True if successful, false otherwise. public async Task DeleteCertificateAsync(string certificateId) { try @@ -410,10 +470,15 @@ public async Task DeleteCertificateAsync(string certificateId) _logger.LogInformation($"Deleted certificate {certificateId}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete certificate - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error deleting certificate: {ex.Message}"); - throw; + _logger.LogError($"Couldn't delete certificate. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DeleteCertificate] @@ -423,7 +488,7 @@ public async Task DeleteCertificateAsync(string certificateId) /// Deletes an IoT Thing. /// /// The name of the Thing to delete. - /// True if successful. + /// True if successful, false otherwise. public async Task DeleteThingAsync(string thingName) { try @@ -437,10 +502,15 @@ public async Task DeleteThingAsync(string thingName) _logger.LogInformation($"Deleted Thing {thingName}"); return true; } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogError($"Cannot delete Thing - resource not found: {ex.Message}"); + return false; + } catch (Exception ex) { - _logger.LogError($"Error deleting Thing: {ex.Message}"); - throw; + _logger.LogError($"Couldn't delete Thing. Here's why: {ex.Message}"); + return false; } } // snippet-end:[iot.dotnetv4.DeleteThing] @@ -449,7 +519,7 @@ public async Task DeleteThingAsync(string thingName) /// /// Lists IoT Things with pagination support. /// - /// List of Things. + /// List of Things, or empty list if listing failed. public async Task> ListThingsAsync() { try @@ -460,10 +530,15 @@ public async Task> ListThingsAsync() _logger.LogInformation($"Retrieved {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.ThrottlingException ex) + { + _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); + return new List(); + } catch (Exception ex) { - _logger.LogError($"Error listing Things: {ex.Message}"); - throw; + _logger.LogError($"Couldn't list Things. Here's why: {ex.Message}"); + return new List(); } } // snippet-end:[iot.dotnetv4.ListThings] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index da7a2243b95..622f82860c1 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -106,33 +106,48 @@ private static async Task RunScenarioAsync() if (createCert?.ToLower() == "y") { - var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); - certificateArn = certArn; - certificateId = certId; - - Console.WriteLine($"\nCertificate:"); - // Show only first few lines of certificate for brevity - var lines = certPem.Split('\n'); - for (int i = 0; i < Math.Min(lines.Length, 5); i++) + var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + if (certificateResult.HasValue) { - Console.WriteLine(lines[i]); - } - if (lines.Length > 5) - { - Console.WriteLine("..."); - } + var (certArn, certPem, certId) = certificateResult.Value; + certificateArn = certArn; + certificateId = certId; + + Console.WriteLine($"\nCertificate:"); + // Show only first few lines of certificate for brevity + var lines = certPem.Split('\n'); + for (int i = 0; i < Math.Min(lines.Length, 5); i++) + { + Console.WriteLine(lines[i]); + } + if (lines.Length > 5) + { + Console.WriteLine("..."); + } - Console.WriteLine($"\nCertificate ARN:"); - Console.WriteLine(certificateArn); + Console.WriteLine($"\nCertificate ARN:"); + Console.WriteLine(certificateArn); - // Step 3: Attach the Certificate to the AWS IoT Thing - Console.WriteLine("Attach the certificate to the AWS IoT Thing."); - await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); - Console.WriteLine("Certificate attached to Thing successfully."); + // Step 3: Attach the Certificate to the AWS IoT Thing + Console.WriteLine("Attach the certificate to the AWS IoT Thing."); + var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + if (attachResult) + { + Console.WriteLine("Certificate attached to Thing successfully."); + } + else + { + Console.WriteLine("Failed to attach certificate to Thing."); + } - Console.WriteLine("Thing Details:"); - Console.WriteLine($"Thing Name: {thingName}"); - Console.WriteLine($"Thing ARN: {thingArn}"); + Console.WriteLine("Thing Details:"); + Console.WriteLine($"Thing Name: {thingName}"); + Console.WriteLine($"Thing ARN: {thingArn}"); + } + else + { + Console.WriteLine("Failed to create certificate."); + } } Console.WriteLine(new string('-', 80)); @@ -165,9 +180,16 @@ private static async Task RunScenarioAsync() Console.ReadLine(); var endpoint = await _iotWrapper.DescribeEndpointAsync(); - var subdomain = endpoint.Split('.')[0]; - Console.WriteLine($"Extracted subdomain: {subdomain}"); - Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + if (endpoint != null) + { + var subdomain = endpoint.Split('.')[0]; + Console.WriteLine($"Extracted subdomain: {subdomain}"); + Console.WriteLine($"Full Endpoint URL: https://{endpoint}"); + } + else + { + Console.WriteLine("Failed to retrieve endpoint."); + } Console.WriteLine(new string('-', 80)); // Step 6: List your AWS IoT certificates diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index 1c52d1f813f..b164a8f90c3 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -65,7 +65,9 @@ public async Task IoTWrapperMethodsTest() // 2. Create a certificate _output.WriteLine("Creating device certificate..."); - var (certArn, certPem, certId) = await _iotWrapper.CreateKeysAndCertificateAsync(); + var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + Assert.True(certificateResult.HasValue); + var (certArn, certPem, certId) = certificateResult.Value; certificateArn = certArn; certificateId = certId; Assert.False(string.IsNullOrEmpty(certificateArn)); From d4a7afe15fa6caefc1eda7a47ec45ff40bf532b2 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:26:01 -0600 Subject: [PATCH 14/26] Updates to classes and specification --- dotnetv4/IoT/Actions/IoTWrapper.cs | 2 +- dotnetv4/IoT/Scenarios/IoTBasics.cs | 392 +++++++++++++++++++--- dotnetv4/IoT/Scenarios/IoTBasics.csproj | 2 + dotnetv4/IoT/Scenarios/appsettings.json | 11 + dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 4 + dotnetv4/IoT/Tests/IoTTests.csproj | 1 + scenarios/basics/iot/SPECIFICATION.md | 8 +- 7 files changed, 369 insertions(+), 51 deletions(-) create mode 100644 dotnetv4/IoT/Scenarios/appsettings.json diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index d0d61b0ed3f..352718ec3da 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -6,7 +6,6 @@ using Amazon.IotData; using Amazon.IotData.Model; using Microsoft.Extensions.Logging; -using System.Text.Json; namespace IoTActions; @@ -542,5 +541,6 @@ public async Task> ListThingsAsync() } } // snippet-end:[iot.dotnetv4.ListThings] + } // snippet-end:[iot.dotnetv4.IoTWrapper] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 622f82860c1..a96050ddf3e 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -1,14 +1,19 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Text.Json; +using Amazon; +using Amazon.Extensions.NETCore.Setup; +using Amazon.IdentityManagement; using Amazon.IoT; +//using Amazon.IoT.Model; using Amazon.IotData; -using Amazon.IoT.Model; +using Amazon.SimpleNotificationService; using IoTActions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Text.Json; namespace IoTScenarios; @@ -18,7 +23,10 @@ namespace IoTScenarios; /// public class IoTBasics { + public static bool IsInteractive = true; private static IoTWrapper _iotWrapper = null!; + private static IAmazonSimpleNotificationService _amazonSNS = null!; + private static IAmazonIdentityManagementService _amazonIAM = null!; private static ILogger _logger = null!; /// @@ -28,20 +36,35 @@ public class IoTBasics /// A Task object. public static async Task Main(string[] args) { + //var config = new ConfigurationBuilder() + // .AddJsonFile("appsettings.json") + // .Build(); + // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => - services.AddAWSService() - .AddAWSService() + services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) + //.AddAWSService(new AWSOptions(){DefaultClientConfig = new AmazonIotDataConfig(){ServiceURL = "https://data.iot.us-east-1.amazonaws.com/"}}) + .AddAWSService() + .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) + .AddSingleton(sp => + { + return new AmazonIotDataClient( + "https://data.iot.us-east-1.amazonaws.com/"); + }) ) .Build(); + + _logger = LoggerFactory.Create(builder => builder.AddConsole()) .CreateLogger(); _iotWrapper = host.Services.GetRequiredService(); + _amazonSNS = host.Services.GetRequiredService(); + _amazonIAM = host.Services.GetRequiredService(); Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); @@ -52,8 +75,11 @@ public static async Task Main(string[] args) Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); Console.WriteLine("developers working with AWS IoT in a .NET environment."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } Console.WriteLine(new string('-', 80)); try @@ -77,10 +103,12 @@ public static async Task Main(string[] args) /// A Task object. private static async Task RunScenarioAsync() { - string thingName = ""; + string thingName = $"iot-thing-{Guid.NewGuid():N}"; string certificateArn = ""; string certificateId = ""; - string ruleName = ""; + string ruleName = $"iot-rule-{Guid.NewGuid():N}"; + string snsTopicArn = ""; + string iotRoleName = ""; try { @@ -89,8 +117,18 @@ private static async Task RunScenarioAsync() Console.WriteLine("1. Create an AWS IoT Thing."); Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); Console.WriteLine(); - Console.Write("Enter Thing name: "); - thingName = Console.ReadLine()!; + + if (IsInteractive) + { + Console.Write("Enter Thing name: "); + var userInput = Console.ReadLine(); + if (!string.IsNullOrEmpty(userInput)) + thingName = userInput; + } + else + { + Console.WriteLine($"Using default Thing name: {thingName}"); + } var thingArn = await _iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); @@ -101,8 +139,17 @@ private static async Task RunScenarioAsync() Console.WriteLine("2. Generate a device certificate."); Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); Console.WriteLine(); - Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); - var createCert = Console.ReadLine(); + + var createCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to create a certificate for {thingName}? (y/n)"); + createCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Creating certificate for {thingName}..."); + } if (createCert?.ToLower() == "y") { @@ -157,8 +204,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var attributes = new Dictionary { @@ -176,8 +226,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("4. Return a unique endpoint specific to the Amazon Web Services account."); Console.WriteLine("An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var endpoint = await _iotWrapper.DescribeEndpointAsync(); if (endpoint != null) @@ -195,8 +248,11 @@ private static async Task RunScenarioAsync() // Step 6: List your AWS IoT certificates Console.WriteLine(new string('-', 80)); Console.WriteLine("5. List your AWS IoT certificates"); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var certificates = await _iotWrapper.ListCertificatesAsync(); foreach (var cert in certificates.Take(5)) // Show first 5 certificates @@ -214,8 +270,11 @@ private static async Task RunScenarioAsync() Console.WriteLine("of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between"); Console.WriteLine("the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow."); Console.WriteLine(); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var shadowPayload = JsonSerializer.Serialize(new { @@ -236,46 +295,75 @@ private static async Task RunScenarioAsync() // Step 8: Write out the state information, in JSON format Console.WriteLine(new string('-', 80)); Console.WriteLine("7. Write out the state information, in JSON format."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); Console.WriteLine($"Received Shadow Data: {shadowData}"); Console.WriteLine(new string('-', 80)); - // Step 9: Creates a rule + // Step 9: Set up resources (SNS topic and IAM role) and create a rule Console.WriteLine(new string('-', 80)); - Console.WriteLine("8. Creates a rule"); + Console.WriteLine("8. Set up resources and create a rule"); Console.WriteLine("Creates a rule that is an administrator-level action."); Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); Console.WriteLine(); - Console.Write("Enter Rule name: "); - ruleName = Console.ReadLine()!; + + if (IsInteractive) + { + Console.Write("Enter Rule name: "); + var userRuleName = Console.ReadLine(); + if (!string.IsNullOrEmpty(userRuleName)) + ruleName = userRuleName; + } + else + { + Console.WriteLine($"Using default rule name: {ruleName}"); + } - // Note: For demonstration, we'll use placeholder ARNs - // In real usage, these should be actual SNS topic and IAM role ARNs - var snsTopicArn = "arn:aws:sns:us-east-1:123456789012:example-topic"; - var roleArn = "arn:aws:iam::123456789012:role/IoTRole"; + // Set up SNS topic and IAM role for the IoT rule + var topicName = $"iot-topic-{Guid.NewGuid():N}"; + iotRoleName = $"iot-role-{Guid.NewGuid():N}"; - Console.WriteLine("Note: Using placeholder ARNs for SNS topic and IAM role."); - Console.WriteLine("In production, ensure these ARNs exist and have proper permissions."); + Console.WriteLine("Setting up SNS topic and IAM role for the IoT rule..."); + var setupResult = await SetupAsync(topicName, iotRoleName); - try + string roleArn = ""; + + if (setupResult.HasValue) { - await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); - Console.WriteLine("IoT Rule created successfully."); + (snsTopicArn, roleArn) = setupResult.Value; + Console.WriteLine($"Successfully created SNS topic: {snsTopicArn}"); + Console.WriteLine($"Successfully created IAM role: {roleArn}"); + + // Now create the IoT rule with the actual ARNs + var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + if (ruleResult) + { + Console.WriteLine("IoT Rule created successfully."); + } + else + { + Console.WriteLine("Failed to create IoT rule."); + } } - catch (Exception ex) + else { - Console.WriteLine($"Note: Rule creation failed (expected with placeholder ARNs): {ex.Message}"); + Console.WriteLine("Failed to set up SNS topic and IAM role. Skipping rule creation."); } Console.WriteLine(new string('-', 80)); // Step 10: List your rules Console.WriteLine(new string('-', 80)); Console.WriteLine("9. List your rules."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var rules = await _iotWrapper.ListTopicRulesAsync(); Console.WriteLine("List of IoT Rules:"); @@ -291,8 +379,11 @@ private static async Task RunScenarioAsync() // Step 11: Search things using the Thing name Console.WriteLine(new string('-', 80)); Console.WriteLine("10. Search things using the Thing name."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); if (searchResults.Any()) @@ -309,14 +400,25 @@ private static async Task RunScenarioAsync() if (!string.IsNullOrEmpty(certificateArn)) { Console.WriteLine(new string('-', 80)); - Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); - var deleteCert = Console.ReadLine(); + var deleteCert = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to detach and delete the certificate for {thingName}? (y/n)"); + deleteCert = Console.ReadLine(); + } + else + { + Console.WriteLine($"Detaching and deleting certificate for {thingName}..."); + } if (deleteCert?.ToLower() == "y") { Console.WriteLine("11. You selected to detach and delete the certificate."); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); @@ -330,8 +432,16 @@ private static async Task RunScenarioAsync() // Step 13: Delete the AWS IoT Thing Console.WriteLine(new string('-', 80)); Console.WriteLine("12. Delete the AWS IoT Thing."); - Console.Write($"Do you want to delete the IoT Thing? (y/n)"); - var deleteThing = Console.ReadLine(); + var deleteThing = "y"; + if (IsInteractive) + { + Console.Write($"Do you want to delete the IoT Thing? (y/n)"); + deleteThing = Console.ReadLine(); + } + else + { + Console.WriteLine($"Deleting IoT Thing {thingName}..."); + } if (deleteThing?.ToLower() == "y") { @@ -339,6 +449,25 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Deleted Thing {thingName}"); } Console.WriteLine(new string('-', 80)); + + // Step 14: Clean up SNS topic and IAM role + if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine("13. Clean up SNS topic and IAM role."); + Console.WriteLine("Cleaning up the resources created for the IoT rule..."); + + var cleanupSuccess = await CleanupAsync(snsTopicArn, iotRoleName); + if (cleanupSuccess) + { + Console.WriteLine("Successfully cleaned up SNS topic and IAM role."); + } + else + { + Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + } + Console.WriteLine(new string('-', 80)); + } } catch (Exception ex) { @@ -369,9 +498,178 @@ private static async Task RunScenarioAsync() _logger.LogError(cleanupEx, "Error during Thing cleanup."); } } + + // Clean up SNS topic and IAM role on error + if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + { + try + { + await CleanupAsync(snsTopicArn, iotRoleName); + } + catch (Exception cleanupEx) + { + _logger.LogError(cleanupEx, "Error during SNS and IAM cleanup."); + } + } throw; } } + + // snippet-start:[iot.dotnetv4.Setup] + /// + /// Sets up the necessary resources for the IoT scenario (SNS topic and IAM role). + /// + /// The name of the SNS topic to create. + /// The name of the IAM role to create. + /// A tuple containing the SNS topic ARN and IAM role ARN, or null if setup failed. + private static async Task<(string SnsTopicArn, string RoleArn)?> SetupAsync(string topicName, string roleName) + { + try + { + // Create SNS topic + var createTopicRequest = new Amazon.SimpleNotificationService.Model.CreateTopicRequest + { + Name = topicName + }; + + var topicResponse = await _amazonSNS.CreateTopicAsync(createTopicRequest); + var snsTopicArn = topicResponse.TopicArn; + _logger.LogInformation($"Created SNS topic {topicName} with ARN {snsTopicArn}"); + + // Create IAM role for IoT + var trustPolicy = @"{ + ""Version"": ""2012-10-17"", + ""Statement"": [ + { + ""Effect"": ""Allow"", + ""Principal"": { + ""Service"": ""iot.amazonaws.com"" + }, + ""Action"": ""sts:AssumeRole"" + } + ] + }"; + + var createRoleRequest = new Amazon.IdentityManagement.Model.CreateRoleRequest + { + RoleName = roleName, + AssumeRolePolicyDocument = trustPolicy, + Description = "Role for AWS IoT to publish to SNS topic" + }; + + var roleResponse = await _amazonIAM.CreateRoleAsync(createRoleRequest); + var roleArn = roleResponse.Role.Arn; + _logger.LogInformation($"Created IAM role {roleName} with ARN {roleArn}"); + + // Attach policy to allow SNS publishing + var policyDocument = $@"{{ + ""Version"": ""2012-10-17"", + ""Statement"": [ + {{ + ""Effect"": ""Allow"", + ""Action"": ""sns:Publish"", + ""Resource"": ""{snsTopicArn}"" + }} + ] + }}"; + + var putRolePolicyRequest = new Amazon.IdentityManagement.Model.PutRolePolicyRequest + { + RoleName = roleName, + PolicyName = "IoTSNSPolicy", + PolicyDocument = policyDocument + }; + + await _amazonIAM.PutRolePolicyAsync(putRolePolicyRequest); + _logger.LogInformation($"Attached SNS policy to role {roleName}"); + + // Wait a bit for the role to propagate + await Task.Delay(10000); + + return (snsTopicArn, roleArn); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't set up resources. Here's why: {ex.Message}"); + return null; + } + } + // snippet-end:[iot.dotnetv4.Setup] + + // snippet-start:[iot.dotnetv4.Cleanup] + /// + /// Cleans up the resources created during setup (SNS topic and IAM role). + /// + /// The ARN of the SNS topic to delete. + /// The name of the IAM role to delete. + /// True if cleanup was successful, false otherwise. + private static async Task CleanupAsync(string snsTopicArn, string roleName) + { + var success = true; + + try + { + // Delete role policy first + try + { + var deleteRolePolicyRequest = new Amazon.IdentityManagement.Model.DeleteRolePolicyRequest + { + RoleName = roleName, + PolicyName = "IoTSNSPolicy" + }; + + await _amazonIAM.DeleteRolePolicyAsync(deleteRolePolicyRequest); + _logger.LogInformation($"Deleted role policy for {roleName}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete role policy: {ex.Message}"); + success = false; + } + + // Delete IAM role + try + { + var deleteRoleRequest = new Amazon.IdentityManagement.Model.DeleteRoleRequest + { + RoleName = roleName + }; + + await _amazonIAM.DeleteRoleAsync(deleteRoleRequest); + _logger.LogInformation($"Deleted IAM role {roleName}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete IAM role: {ex.Message}"); + success = false; + } + + // Delete SNS topic + try + { + var deleteTopicRequest = new Amazon.SimpleNotificationService.Model.DeleteTopicRequest + { + TopicArn = snsTopicArn + }; + + await _amazonSNS.DeleteTopicAsync(deleteTopicRequest); + _logger.LogInformation($"Deleted SNS topic {snsTopicArn}"); + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to delete SNS topic: {ex.Message}"); + success = false; + } + + return success; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't clean up resources. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.Cleanup] } // snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index bc870c03ebf..41a0573fb20 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -10,6 +10,8 @@ + + diff --git a/dotnetv4/IoT/Scenarios/appsettings.json b/dotnetv4/IoT/Scenarios/appsettings.json new file mode 100644 index 00000000000..a84bda6ba5b --- /dev/null +++ b/dotnetv4/IoT/Scenarios/appsettings.json @@ -0,0 +1,11 @@ +{ + "AwsIotDataConfig": { + "Profile": "default", + "ServiceURL": "https://data.iot.us-east-1.amazonaws.com/" + }, + + "AwsConfig": { + "Profile": "default", + "Region": "us-east-1" + } +} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index b164a8f90c3..f5c4728218f 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -4,6 +4,7 @@ using Amazon.IoT; using Amazon.IotData; using IoTActions; +using IoTScenarios; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -49,6 +50,9 @@ public IoTIntegrationTests(ITestOutputHelper output) [Trait("Category", "Integration")] public async Task IoTWrapperMethodsTest() { + // Set to non-interactive mode for testing + IoTScenarios.IoTBasics.IsInteractive = false; + var thingName = $"test-thing-{Guid.NewGuid():N}"; var certificateArn = ""; var certificateId = ""; diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index 82596e09888..ed1b6b951ca 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -29,6 +29,7 @@ + diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 891e4123d60..bc7aaa6efe0 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,10 +6,12 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program requires these AWS resources. +This program should create and manage these AWS resources automatically: -1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. -2. **snsAction** - An ARN of an SNS topic. +1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. This role must be automatically created during the scenario execution with proper permissions to publish to SNS topics. +2. **snsAction** - An ARN of an SNS topic. This topic must be automatically created during the scenario execution for use with IoT rules. + +Both resources must be created during scenario setup and automatically cleaned up at the end of the scenario execution. ## Hello AWS IoT From a11193af43ad06552f39d4a4f323457bfb66314d Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:39:25 -0600 Subject: [PATCH 15/26] Updates to setup and basics. --- dotnetv4/IoT/Actions/IoTWrapper.cs | 10 +- dotnetv4/IoT/Scenarios/IoTBasics.cs | 424 ++++++++++++++-------- dotnetv4/IoT/Scenarios/IoTBasics.csproj | 1 + dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 3 +- scenarios/basics/iot/SPECIFICATION.md | 14 +- 5 files changed, 287 insertions(+), 165 deletions(-) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 352718ec3da..738cd8ac985 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -384,9 +384,17 @@ public async Task> SearchIndexAsync(string queryString) { try { + await _amazonIoT.UpdateIndexingConfigurationAsync( + new UpdateIndexingConfigurationRequest() + { + ThingIndexingConfiguration = new ThingIndexingConfiguration() + { + ThingIndexingMode = ThingIndexingMode.REGISTRY + } + }); + var request = new SearchIndexRequest { - IndexName = "AWS_Things", QueryString = queryString }; diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index a96050ddf3e..2aa59af617a 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -3,19 +3,18 @@ using System.Text.Json; using Amazon; +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; using Amazon.Extensions.NETCore.Setup; -using Amazon.IdentityManagement; using Amazon.IoT; -//using Amazon.IoT.Model; +using Amazon.IoT.Model; using Amazon.IotData; -using Amazon.SimpleNotificationService; using IoTActions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace IoTScenarios; +namespace IoTBasics; // snippet-start:[iot.dotnetv4.IoTScenario] /// @@ -25,10 +24,12 @@ public class IoTBasics { public static bool IsInteractive = true; private static IoTWrapper _iotWrapper = null!; - private static IAmazonSimpleNotificationService _amazonSNS = null!; - private static IAmazonIdentityManagementService _amazonIAM = null!; + private static IAmazonCloudFormation _amazonCloudFormation = null!; private static ILogger _logger = null!; + private static string _stackName = "IoTBasicsStack"; + private static string _stackResourcePath = "../../../../../../scenarios/basics/iot/iot_usecase/resources/cfn_template.yaml"; + /// /// Main method for the IoT Basics scenario. /// @@ -44,15 +45,18 @@ public static async Task Main(string[] args) using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) - //.AddAWSService(new AWSOptions(){DefaultClientConfig = new AmazonIotDataConfig(){ServiceURL = "https://data.iot.us-east-1.amazonaws.com/"}}) - .AddAWSService() - .AddAWSService() + .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) .AddSingleton(sp => { - return new AmazonIotDataClient( - "https://data.iot.us-east-1.amazonaws.com/"); + var iotService = sp.GetService(); + var request = new DescribeEndpointRequest + { + EndpointType = "iot:Data-ATS" + }; + var response = iotService.DescribeEndpointAsync(request).Result; + return new AmazonIotDataClient($"https://{response.EndpointAddress}/"); }) ) .Build(); @@ -63,17 +67,11 @@ public static async Task Main(string[] args) .CreateLogger(); _iotWrapper = host.Services.GetRequiredService(); - _amazonSNS = host.Services.GetRequiredService(); - _amazonIAM = host.Services.GetRequiredService(); + _amazonCloudFormation = host.Services.GetRequiredService(); Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); - Console.WriteLine("The program guides you through a series of steps, including creating an IoT Thing, generating a device certificate,"); - Console.WriteLine("updating the Thing with attributes, and so on. It utilizes the AWS SDK for .NET and incorporates functionalities"); - Console.WriteLine("for creating and managing IoT Things, certificates, rules, shadows, and performing searches."); - Console.WriteLine("The program aims to showcase AWS IoT capabilities and provides a comprehensive example for"); - Console.WriteLine("developers working with AWS IoT in a .NET environment."); Console.WriteLine(); if (IsInteractive) { @@ -108,7 +106,6 @@ private static async Task RunScenarioAsync() string certificateId = ""; string ruleName = $"iot-rule-{Guid.NewGuid():N}"; string snsTopicArn = ""; - string iotRoleName = ""; try { @@ -324,35 +321,52 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Using default rule name: {ruleName}"); } - // Set up SNS topic and IAM role for the IoT rule - var topicName = $"iot-topic-{Guid.NewGuid():N}"; - iotRoleName = $"iot-role-{Guid.NewGuid():N}"; - - Console.WriteLine("Setting up SNS topic and IAM role for the IoT rule..."); - var setupResult = await SetupAsync(topicName, iotRoleName); + // Deploy CloudFormation stack to create SNS topic and IAM role + Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); - string roleArn = ""; - - if (setupResult.HasValue) + var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); + if (deployStack) { - (snsTopicArn, roleArn) = setupResult.Value; - Console.WriteLine($"Successfully created SNS topic: {snsTopicArn}"); - Console.WriteLine($"Successfully created IAM role: {roleArn}"); - - // Now create the IoT rule with the actual ARNs - var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); - if (ruleResult) + _stackName = PromptUserForStackName(); + + var deploySuccess = await DeployCloudFormationStack(_stackName); + + if (deploySuccess) { - Console.WriteLine("IoT Rule created successfully."); + // Get stack outputs + var stackOutputs = await GetStackOutputs(_stackName); + if (stackOutputs != null) + { + snsTopicArn = stackOutputs["SNSTopicArn"]; + string roleArn = stackOutputs["RoleArn"]; + + Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); + Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); + + // Now create the IoT rule with the CloudFormation outputs + var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + if (ruleResult) + { + Console.WriteLine("IoT Rule created successfully."); + } + else + { + Console.WriteLine("Failed to create IoT rule."); + } + } + else + { + Console.WriteLine("Failed to get stack outputs. Skipping rule creation."); + } } else { - Console.WriteLine("Failed to create IoT rule."); + Console.WriteLine("Failed to deploy CloudFormation stack. Skipping rule creation."); } } else { - Console.WriteLine("Failed to set up SNS topic and IAM role. Skipping rule creation."); + Console.WriteLine("Skipping CloudFormation stack deployment and rule creation."); } Console.WriteLine(new string('-', 80)); @@ -450,21 +464,29 @@ private static async Task RunScenarioAsync() } Console.WriteLine(new string('-', 80)); - // Step 14: Clean up SNS topic and IAM role - if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + // Step 14: Clean up CloudFormation stack + if (!string.IsNullOrEmpty(snsTopicArn)) { Console.WriteLine(new string('-', 80)); - Console.WriteLine("13. Clean up SNS topic and IAM role."); - Console.WriteLine("Cleaning up the resources created for the IoT rule..."); + Console.WriteLine("13. Clean up CloudFormation stack."); + Console.WriteLine("Deleting the CloudFormation stack and all resources..."); - var cleanupSuccess = await CleanupAsync(snsTopicArn, iotRoleName); - if (cleanupSuccess) + var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); + if (cleanup) { - Console.WriteLine("Successfully cleaned up SNS topic and IAM role."); + var cleanupSuccess = await DeleteCloudFormationStack(_stackName); + if (cleanupSuccess) + { + Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); + } + else + { + Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + } } else { - Console.WriteLine("Some cleanup operations failed. Check the logs for details."); + Console.WriteLine($"Resources will remain. Stack name: {_stackName}"); } Console.WriteLine(new string('-', 80)); } @@ -499,16 +521,16 @@ private static async Task RunScenarioAsync() } } - // Clean up SNS topic and IAM role on error - if (!string.IsNullOrEmpty(snsTopicArn) && !string.IsNullOrEmpty(iotRoleName)) + // Clean up CloudFormation stack on error + if (!string.IsNullOrEmpty(snsTopicArn)) { try { - await CleanupAsync(snsTopicArn, iotRoleName); + await DeleteCloudFormationStack(_stackName); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during SNS and IAM cleanup."); + _logger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } @@ -516,160 +538,246 @@ private static async Task RunScenarioAsync() } } - // snippet-start:[iot.dotnetv4.Setup] /// - /// Sets up the necessary resources for the IoT scenario (SNS topic and IAM role). + /// Deploys the CloudFormation stack with the necessary resources. /// - /// The name of the SNS topic to create. - /// The name of the IAM role to create. - /// A tuple containing the SNS topic ARN and IAM role ARN, or null if setup failed. - private static async Task<(string SnsTopicArn, string RoleArn)?> SetupAsync(string topicName, string roleName) + /// The name of the CloudFormation stack. + /// True if the stack was deployed successfully. + private static async Task DeployCloudFormationStack(string stackName) { + Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); + try { - // Create SNS topic - var createTopicRequest = new Amazon.SimpleNotificationService.Model.CreateTopicRequest + var request = new CreateStackRequest { - Name = topicName + StackName = stackName, + TemplateBody = await File.ReadAllTextAsync(_stackResourcePath), + Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } }; - var topicResponse = await _amazonSNS.CreateTopicAsync(createTopicRequest); - var snsTopicArn = topicResponse.TopicArn; - _logger.LogInformation($"Created SNS topic {topicName} with ARN {snsTopicArn}"); + var response = await _amazonCloudFormation.CreateStackAsync(request); - // Create IAM role for IoT - var trustPolicy = @"{ - ""Version"": ""2012-10-17"", - ""Statement"": [ - { - ""Effect"": ""Allow"", - ""Principal"": { - ""Service"": ""iot.amazonaws.com"" - }, - ""Action"": ""sts:AssumeRole"" - } - ] - }"; + if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + Console.WriteLine($"CloudFormation stack creation started: {stackName}"); + + bool stackCreated = await WaitForStackCompletion(response.StackId); + + if (stackCreated) + { + Console.WriteLine("CloudFormation stack created successfully."); + return true; + } + else + { + _logger.LogError($"CloudFormation stack creation failed: {stackName}"); + return false; + } + } + else + { + _logger.LogError($"Failed to create CloudFormation stack: {stackName}"); + return false; + } + } + catch (AlreadyExistsException) + { + _logger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); + var newStackName = PromptUserForStackName(); + return await DeployCloudFormationStack(newStackName); + } + catch (Exception ex) + { + _logger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); + return false; + } + } - var createRoleRequest = new Amazon.IdentityManagement.Model.CreateRoleRequest + /// + /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. + /// + /// The ID of the CloudFormation stack. + /// True if the stack was created successfully. + private static async Task WaitForStackCompletion(string stackId) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; + + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest { - RoleName = roleName, - AssumeRolePolicyDocument = trustPolicy, - Description = "Role for AWS IoT to publish to SNS topic" + StackName = stackId }; - var roleResponse = await _amazonIAM.CreateRoleAsync(createRoleRequest); - var roleArn = roleResponse.Role.Arn; - _logger.LogInformation($"Created IAM role {roleName} with ARN {roleArn}"); - - // Attach policy to allow SNS publishing - var policyDocument = $@"{{ - ""Version"": ""2012-10-17"", - ""Statement"": [ - {{ - ""Effect"": ""Allow"", - ""Action"": ""sns:Publish"", - ""Resource"": ""{snsTopicArn}"" - }} - ] - }}"; - - var putRolePolicyRequest = new Amazon.IdentityManagement.Model.PutRolePolicyRequest - { - RoleName = roleName, - PolicyName = "IoTSNSPolicy", - PolicyDocument = policyDocument - }; + var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + + if (describeStacksResponse.Stacks.Count > 0) + { + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_COMPLETE) + { + return true; + } + if (describeStacksResponse.Stacks[0].StackStatus == StackStatus.CREATE_FAILED || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + return false; + } + } - await _amazonIAM.PutRolePolicyAsync(putRolePolicyRequest); - _logger.LogInformation($"Attached SNS policy to role {roleName}"); + Console.WriteLine("Waiting for CloudFormation stack creation to complete..."); + await Task.Delay(retryDelay); + retryCount++; + } - // Wait a bit for the role to propagate - await Task.Delay(10000); + _logger.LogError("Timed out waiting for CloudFormation stack creation to complete."); + return false; + } + + /// + /// Gets the outputs from the CloudFormation stack. + /// + /// The name of the CloudFormation stack. + /// A dictionary of stack outputs. + private static async Task?> GetStackOutputs(string stackName) + { + try + { + var describeStacksRequest = new DescribeStacksRequest + { + StackName = stackName + }; - return (snsTopicArn, roleArn); + var response = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + + if (response.Stacks.Count > 0) + { + var outputs = new Dictionary(); + foreach (var output in response.Stacks[0].Outputs) + { + outputs[output.OutputKey] = output.OutputValue; + } + return outputs; + } + + return null; } catch (Exception ex) { - _logger.LogError($"Couldn't set up resources. Here's why: {ex.Message}"); + _logger.LogError(ex, $"Failed to get stack outputs for {stackName}"); return null; } } - // snippet-end:[iot.dotnetv4.Setup] - // snippet-start:[iot.dotnetv4.Cleanup] /// - /// Cleans up the resources created during setup (SNS topic and IAM role). + /// Deletes the CloudFormation stack and waits for confirmation. /// - /// The ARN of the SNS topic to delete. - /// The name of the IAM role to delete. - /// True if cleanup was successful, false otherwise. - private static async Task CleanupAsync(string snsTopicArn, string roleName) + private static async Task DeleteCloudFormationStack(string stackName) { - var success = true; - try { - // Delete role policy first - try + var request = new DeleteStackRequest { - var deleteRolePolicyRequest = new Amazon.IdentityManagement.Model.DeleteRolePolicyRequest - { - RoleName = roleName, - PolicyName = "IoTSNSPolicy" - }; + StackName = stackName + }; + + await _amazonCloudFormation.DeleteStackAsync(request); + Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); + + bool stackDeleted = await WaitForStackDeletion(stackName); - await _amazonIAM.DeleteRolePolicyAsync(deleteRolePolicyRequest); - _logger.LogInformation($"Deleted role policy for {roleName}"); + if (stackDeleted) + { + Console.WriteLine($"CloudFormation stack '{stackName}' has been deleted."); + return true; } - catch (Exception ex) + else { - _logger.LogWarning($"Failed to delete role policy: {ex.Message}"); - success = false; + _logger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); + return false; } + } + catch (Exception ex) + { + _logger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); + return false; + } + } - // Delete IAM role - try - { - var deleteRoleRequest = new Amazon.IdentityManagement.Model.DeleteRoleRequest - { - RoleName = roleName - }; + /// + /// Waits for the stack to be deleted. + /// + private static async Task WaitForStackDeletion(string stackName) + { + int retryCount = 0; + const int maxRetries = 30; + const int retryDelay = 10000; - await _amazonIAM.DeleteRoleAsync(deleteRoleRequest); - _logger.LogInformation($"Deleted IAM role {roleName}"); - } - catch (Exception ex) + while (retryCount < maxRetries) + { + var describeStacksRequest = new DescribeStacksRequest { - _logger.LogWarning($"Failed to delete IAM role: {ex.Message}"); - success = false; - } + StackName = stackName + }; - // Delete SNS topic try { - var deleteTopicRequest = new Amazon.SimpleNotificationService.Model.DeleteTopicRequest - { - TopicArn = snsTopicArn - }; + var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); - await _amazonSNS.DeleteTopicAsync(deleteTopicRequest); - _logger.LogInformation($"Deleted SNS topic {snsTopicArn}"); + if (describeStacksResponse.Stacks.Count == 0 || + describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) + { + return true; + } } - catch (Exception ex) + catch (AmazonCloudFormationException ex) when (ex.ErrorCode == "ValidationError") { - _logger.LogWarning($"Failed to delete SNS topic: {ex.Message}"); - success = false; + return true; } - return success; + Console.WriteLine($"Waiting for CloudFormation stack '{stackName}' to be deleted..."); + await Task.Delay(retryDelay); + retryCount++; } - catch (Exception ex) + + _logger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); + return false; + } + + /// + /// Helper method to get a yes or no response from the user. + /// + private static bool GetYesNoResponse(string question) + { + Console.WriteLine(question); + var ynResponse = Console.ReadLine(); + var response = ynResponse != null && ynResponse.Equals("y", StringComparison.InvariantCultureIgnoreCase); + return response; + } + + /// + /// Prompts the user for a stack name. + /// + private static string PromptUserForStackName() + { + if (IsInteractive) { - _logger.LogError($"Couldn't clean up resources. Here's why: {ex.Message}"); - return false; + Console.Write($"Enter a name for the CloudFormation stack (press Enter for default '{_stackName}'): "); + string? input = Console.ReadLine(); + if (!string.IsNullOrWhiteSpace(input)) + { + var regex = new System.Text.RegularExpressions.Regex("[a-zA-Z][-a-zA-Z0-9]*"); + if (!regex.IsMatch(input)) + { + Console.WriteLine($"Invalid stack name. Using default: {_stackName}"); + return _stackName; + } + return input; + } } + return _stackName; } - // snippet-end:[iot.dotnetv4.Cleanup] } // snippet-end:[iot.dotnetv4.IoTScenario] diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index 41a0573fb20..16c59301e29 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -12,6 +12,7 @@ + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index f5c4728218f..aea78aeb5b8 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -4,7 +4,6 @@ using Amazon.IoT; using Amazon.IotData; using IoTActions; -using IoTScenarios; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -51,7 +50,7 @@ public IoTIntegrationTests(ITestOutputHelper output) public async Task IoTWrapperMethodsTest() { // Set to non-interactive mode for testing - IoTScenarios.IoTBasics.IsInteractive = false; + IoTBasics.IoTBasics.IsInteractive = false; var thingName = $"test-thing-{Guid.NewGuid():N}"; var certificateArn = ""; diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index bc7aaa6efe0..09399bd280f 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,12 +6,18 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program should create and manage these AWS resources automatically: +This program should create and manage these AWS resources automatically using CloudFormation: -1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IOT. This role must be automatically created during the scenario execution with proper permissions to publish to SNS topics. -2. **snsAction** - An ARN of an SNS topic. This topic must be automatically created during the scenario execution for use with IoT rules. +1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IoT. This role is created through CloudFormation stack deployment with proper permissions to publish to SNS topics. +2. **snsAction** - An ARN of an SNS topic. This topic is created through CloudFormation stack deployment for use with IoT rules. -Both resources must be created during scenario setup and automatically cleaned up at the end of the scenario execution. +### CloudFormation Integration +- **Setup**: The scenario deploys a CloudFormation stack using the template file `iot_usecase/resources/cfn_template.yaml` +- **Resource Creation**: All required resources (SNS topic and IAM role) are defined in the CloudFormation template +- **Output Retrieval**: The scenario retrieves the SNS topic ARN and IAM role ARN from the CloudFormation stack outputs +- **Cleanup**: At the end of the scenario execution, the entire CloudFormation stack is deleted, ensuring all resources are properly cleaned up + +The CloudFormation template provides Infrastructure as Code (IaC) benefits, ensuring consistent and repeatable resource deployment across different environments. ## Hello AWS IoT From a39da4e59d98c5e19f492168b0d6da1385ee3056 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:40:30 -0600 Subject: [PATCH 16/26] Fix search setup. --- dotnetv4/IoT/Actions/IoTWrapper.cs | 92 ++++++++++++++++++++++++--- scenarios/basics/iot/SPECIFICATION.md | 9 ++- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 738cd8ac985..ae9290a475f 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -384,15 +384,7 @@ public async Task> SearchIndexAsync(string queryString) { try { - await _amazonIoT.UpdateIndexingConfigurationAsync( - new UpdateIndexingConfigurationRequest() - { - ThingIndexingConfiguration = new ThingIndexingConfiguration() - { - ThingIndexingMode = ThingIndexingMode.REGISTRY - } - }); - + // First, try to perform the search var request = new SearchIndexRequest { QueryString = queryString @@ -402,6 +394,16 @@ await _amazonIoT.UpdateIndexingConfigurationAsync( _logger.LogInformation($"Search found {response.Things.Count} Things"); return response.Things; } + catch (Amazon.IoT.Model.IndexNotReadyException ex) + { + _logger.LogWarning($"Search index not ready, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + _logger.LogWarning($"Search index not configured, setting up indexing configuration: {ex.Message}"); + return await SetupIndexAndRetrySearchAsync(queryString); + } catch (Amazon.IoT.Model.ThrottlingException ex) { _logger.LogWarning($"Request throttled, please try again later: {ex.Message}"); @@ -413,6 +415,78 @@ await _amazonIoT.UpdateIndexingConfigurationAsync( return new List(); } } + + /// + /// Sets up the indexing configuration and retries the search after waiting for the index to be ready. + /// + /// The search query string. + /// List of Things that match the search criteria, or empty list if setup/search failed. + private async Task> SetupIndexAndRetrySearchAsync(string queryString) + { + try + { + // Update indexing configuration to REGISTRY mode + _logger.LogInformation("Setting up IoT search indexing configuration..."); + await _amazonIoT.UpdateIndexingConfigurationAsync( + new UpdateIndexingConfigurationRequest() + { + ThingIndexingConfiguration = new ThingIndexingConfiguration() + { + ThingIndexingMode = ThingIndexingMode.REGISTRY + } + }); + + _logger.LogInformation("Indexing configuration updated. Waiting for index to be ready..."); + + // Wait for the index to be set up - this can take some time + const int maxRetries = 10; + const int retryDelaySeconds = 10; + + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + _logger.LogInformation($"Waiting for index to be ready (attempt {attempt}/{maxRetries})..."); + await Task.Delay(TimeSpan.FromSeconds(retryDelaySeconds)); + + // Try to get the current indexing configuration to see if it's ready + var configResponse = await _amazonIoT.GetIndexingConfigurationAsync(new GetIndexingConfigurationRequest()); + if (configResponse.ThingIndexingConfiguration?.ThingIndexingMode == ThingIndexingMode.REGISTRY) + { + // Try the search again + var request = new SearchIndexRequest + { + QueryString = queryString + }; + + var response = await _amazonIoT.SearchIndexAsync(request); + _logger.LogInformation($"Search found {response.Things.Count} Things after index setup"); + return response.Things; + } + } + catch (Amazon.IoT.Model.IndexNotReadyException) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + catch (Amazon.IoT.Model.InvalidRequestException ex) when (ex.Message.Contains("index") || ex.Message.Contains("Index")) + { + // Index still not ready, continue waiting + _logger.LogInformation("Index still not ready, continuing to wait..."); + continue; + } + } + + _logger.LogWarning("Timeout waiting for search index to be ready after configuration update"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError($"Couldn't set up search index configuration. Here's why: {ex.Message}"); + return new List(); + } + } // snippet-end:[iot.dotnetv4.SearchIndex] // snippet-start:[iot.dotnetv4.DetachThingPrincipal] diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 09399bd280f..6ec42575059 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -69,7 +69,14 @@ This scenario demonstrates the following key AWS IoT Service operations: - Use the `ListTopicRules` API to retrieve a list of all AWS IoT Rules. 12. **Search AWS IoT Things**: - - Use the `SearchThings` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. + - Use the `SearchIndex` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. + - **Automatic Index Configuration**: The search functionality includes intelligent handling of index setup: + - If the search index is not configured, the system automatically detects this condition through exception handling + - Catches `IndexNotReadyException` and `InvalidRequestException` that indicate the search index needs to be set up + - Automatically configures the Thing indexing mode to `REGISTRY` to enable search functionality + - Implements a retry mechanism with up to 10 attempts, waiting 10 seconds between each attempt for the index to become ready + - Validates the indexing configuration status before retrying search operations + - Provides detailed logging throughout the index setup process to keep users informed of progress 13. **Delete an AWS IoT Thing**: - Use the `DeleteThing` API to delete an AWS IoT Thing. From c606dfe5149fb60c2e0337ab403cc71851c3c189 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:06:03 -0600 Subject: [PATCH 17/26] Updates to integration tests. --- dotnetv4/IoT/Scenarios/IoTBasics.cs | 147 ++++++++++------- dotnetv4/IoT/Scenarios/appsettings.json | 11 -- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 186 +++++----------------- dotnetv4/IoT/Tests/IoTTests.csproj | 1 + 4 files changed, 132 insertions(+), 213 deletions(-) delete mode 100644 dotnetv4/IoT/Scenarios/appsettings.json diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 2aa59af617a..305945e7138 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -23,6 +23,9 @@ namespace IoTBasics; public class IoTBasics { public static bool IsInteractive = true; + public static IoTWrapper? Wrapper = null; + public static IAmazonCloudFormation? CloudFormationClient = null; + public static ILogger logger = null!; private static IoTWrapper _iotWrapper = null!; private static IAmazonCloudFormation _amazonCloudFormation = null!; private static ILogger _logger = null!; @@ -37,10 +40,6 @@ public class IoTBasics /// A Task object. public static async Task Main(string[] args) { - //var config = new ConfigurationBuilder() - // .AddJsonFile("appsettings.json") - // .Build(); - // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => @@ -61,13 +60,16 @@ public static async Task Main(string[] args) ) .Build(); - - - _logger = LoggerFactory.Create(builder => builder.AddConsole()) + logger = LoggerFactory.Create(builder => builder.AddConsole()) .CreateLogger(); - _iotWrapper = host.Services.GetRequiredService(); - _amazonCloudFormation = host.Services.GetRequiredService(); + Wrapper = host.Services.GetRequiredService(); + CloudFormationClient = host.Services.GetRequiredService(); + + // Set the private fields for backwards compatibility + _logger = logger; + _iotWrapper = Wrapper; + _amazonCloudFormation = CloudFormationClient; Console.WriteLine(new string('-', 80)); Console.WriteLine("Welcome to the AWS IoT example workflow."); @@ -99,7 +101,24 @@ public static async Task Main(string[] args) /// Run the IoT Basics scenario. /// /// A Task object. - private static async Task RunScenarioAsync() + public static async Task RunScenarioAsync() + { + // Use static properties if available, otherwise use private fields + var iotWrapper = Wrapper ?? _iotWrapper; + var cloudFormationClient = CloudFormationClient ?? _amazonCloudFormation; + var scenarioLogger = logger ?? _logger; + + await RunScenarioInternalAsync(iotWrapper, cloudFormationClient, scenarioLogger); + } + + /// + /// Internal method to run the IoT Basics scenario with injected dependencies. + /// + /// The IoT wrapper instance. + /// The CloudFormation client instance. + /// The logger instance. + /// A Task object. + private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { string thingName = $"iot-thing-{Guid.NewGuid():N}"; string certificateArn = ""; @@ -127,7 +146,7 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Using default Thing name: {thingName}"); } - var thingArn = await _iotWrapper.CreateThingAsync(thingName); + var thingArn = await iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); Console.WriteLine(new string('-', 80)); @@ -150,7 +169,7 @@ private static async Task RunScenarioAsync() if (createCert?.ToLower() == "y") { - var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); + var certificateResult = await iotWrapper.CreateKeysAndCertificateAsync(); if (certificateResult.HasValue) { var (certArn, certPem, certId) = certificateResult.Value; @@ -174,7 +193,7 @@ private static async Task RunScenarioAsync() // Step 3: Attach the Certificate to the AWS IoT Thing Console.WriteLine("Attach the certificate to the AWS IoT Thing."); - var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); + var attachResult = await iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); if (attachResult) { Console.WriteLine("Certificate attached to Thing successfully."); @@ -214,7 +233,7 @@ private static async Task RunScenarioAsync() { "Firmware", "1.2.3" } }; - await _iotWrapper.UpdateThingAsync(thingName, attributes); + await iotWrapper.UpdateThingAsync(thingName, attributes); Console.WriteLine("Thing attributes updated successfully."); Console.WriteLine(new string('-', 80)); @@ -229,7 +248,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var endpoint = await _iotWrapper.DescribeEndpointAsync(); + var endpoint = await iotWrapper.DescribeEndpointAsync(); if (endpoint != null) { var subdomain = endpoint.Split('.')[0]; @@ -251,7 +270,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var certificates = await _iotWrapper.ListCertificatesAsync(); + var certificates = await iotWrapper.ListCertificatesAsync(); foreach (var cert in certificates.Take(5)) // Show first 5 certificates { Console.WriteLine($"Cert id: {cert.CertificateId}"); @@ -285,7 +304,7 @@ private static async Task RunScenarioAsync() } }); - await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); + await iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); Console.WriteLine("Thing Shadow updated successfully."); Console.WriteLine(new string('-', 80)); @@ -298,7 +317,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); + var shadowData = await iotWrapper.GetThingShadowAsync(thingName); Console.WriteLine($"Received Shadow Data: {shadowData}"); Console.WriteLine(new string('-', 80)); @@ -329,12 +348,12 @@ private static async Task RunScenarioAsync() { _stackName = PromptUserForStackName(); - var deploySuccess = await DeployCloudFormationStack(_stackName); + var deploySuccess = await DeployCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); if (deploySuccess) { // Get stack outputs - var stackOutputs = await GetStackOutputs(_stackName); + var stackOutputs = await GetStackOutputs(_stackName, cloudFormationClient, scenarioLogger); if (stackOutputs != null) { snsTopicArn = stackOutputs["SNSTopicArn"]; @@ -344,7 +363,7 @@ private static async Task RunScenarioAsync() Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); // Now create the IoT rule with the CloudFormation outputs - var ruleResult = await _iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); + var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); if (ruleResult) { Console.WriteLine("IoT Rule created successfully."); @@ -379,7 +398,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var rules = await _iotWrapper.ListTopicRulesAsync(); + var rules = await iotWrapper.ListTopicRulesAsync(); Console.WriteLine("List of IoT Rules:"); foreach (var rule in rules.Take(5)) // Show first 5 rules { @@ -399,7 +418,7 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); + var searchResults = await iotWrapper.SearchIndexAsync($"thingName:{thingName}"); if (searchResults.Any()) { Console.WriteLine($"Thing id found using search is {searchResults.First().ThingId}"); @@ -434,10 +453,10 @@ private static async Task RunScenarioAsync() Console.ReadLine(); } - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); Console.WriteLine($"{certificateArn} was successfully removed from {thingName}"); - await _iotWrapper.DeleteCertificateAsync(certificateId); + await iotWrapper.DeleteCertificateAsync(certificateId); Console.WriteLine($"{certificateArn} was successfully deleted."); } Console.WriteLine(new string('-', 80)); @@ -459,7 +478,7 @@ private static async Task RunScenarioAsync() if (deleteThing?.ToLower() == "y") { - await _iotWrapper.DeleteThingAsync(thingName); + await iotWrapper.DeleteThingAsync(thingName); Console.WriteLine($"Deleted Thing {thingName}"); } Console.WriteLine(new string('-', 80)); @@ -474,7 +493,7 @@ private static async Task RunScenarioAsync() var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { - var cleanupSuccess = await DeleteCloudFormationStack(_stackName); + var cleanupSuccess = await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); if (cleanupSuccess) { Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); @@ -493,19 +512,19 @@ private static async Task RunScenarioAsync() } catch (Exception ex) { - _logger.LogError(ex, "Error occurred during scenario execution."); + scenarioLogger.LogError(ex, "Error occurred during scenario execution."); // Cleanup on error if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) { try { - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); - await _iotWrapper.DeleteCertificateAsync(certificateId); + await iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); + await iotWrapper.DeleteCertificateAsync(certificateId); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during cleanup."); } } @@ -513,11 +532,11 @@ private static async Task RunScenarioAsync() { try { - await _iotWrapper.DeleteThingAsync(thingName); + await iotWrapper.DeleteThingAsync(thingName); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during Thing cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during Thing cleanup."); } } @@ -526,11 +545,11 @@ private static async Task RunScenarioAsync() { try { - await DeleteCloudFormationStack(_stackName); + await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); } catch (Exception cleanupEx) { - _logger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); + scenarioLogger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } @@ -542,8 +561,10 @@ private static async Task RunScenarioAsync() /// Deploys the CloudFormation stack with the necessary resources. /// /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// True if the stack was deployed successfully. - private static async Task DeployCloudFormationStack(string stackName) + private static async Task DeployCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { Console.WriteLine($"\nDeploying CloudFormation stack: {stackName}"); @@ -556,13 +577,13 @@ private static async Task DeployCloudFormationStack(string stackName) Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } }; - var response = await _amazonCloudFormation.CreateStackAsync(request); + var response = await cloudFormationClient.CreateStackAsync(request); if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) { Console.WriteLine($"CloudFormation stack creation started: {stackName}"); - bool stackCreated = await WaitForStackCompletion(response.StackId); + bool stackCreated = await WaitForStackCompletion(response.StackId, cloudFormationClient, scenarioLogger); if (stackCreated) { @@ -571,25 +592,25 @@ private static async Task DeployCloudFormationStack(string stackName) } else { - _logger.LogError($"CloudFormation stack creation failed: {stackName}"); + scenarioLogger.LogError($"CloudFormation stack creation failed: {stackName}"); return false; } } else { - _logger.LogError($"Failed to create CloudFormation stack: {stackName}"); + scenarioLogger.LogError($"Failed to create CloudFormation stack: {stackName}"); return false; } } catch (AlreadyExistsException) { - _logger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); + scenarioLogger.LogWarning($"CloudFormation stack '{stackName}' already exists. Please provide a unique name."); var newStackName = PromptUserForStackName(); - return await DeployCloudFormationStack(newStackName); + return await DeployCloudFormationStack(newStackName, cloudFormationClient, scenarioLogger); } catch (Exception ex) { - _logger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); + scenarioLogger.LogError(ex, $"An error occurred while deploying the CloudFormation stack: {stackName}"); return false; } } @@ -598,8 +619,10 @@ private static async Task DeployCloudFormationStack(string stackName) /// Waits for the CloudFormation stack to be in the CREATE_COMPLETE state. /// /// The ID of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// True if the stack was created successfully. - private static async Task WaitForStackCompletion(string stackId) + private static async Task WaitForStackCompletion(string stackId, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { int retryCount = 0; const int maxRetries = 30; @@ -612,7 +635,7 @@ private static async Task WaitForStackCompletion(string stackId) StackName = stackId }; - var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count > 0) { @@ -632,7 +655,7 @@ private static async Task WaitForStackCompletion(string stackId) retryCount++; } - _logger.LogError("Timed out waiting for CloudFormation stack creation to complete."); + scenarioLogger.LogError("Timed out waiting for CloudFormation stack creation to complete."); return false; } @@ -640,8 +663,10 @@ private static async Task WaitForStackCompletion(string stackId) /// Gets the outputs from the CloudFormation stack. /// /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. /// A dictionary of stack outputs. - private static async Task?> GetStackOutputs(string stackName) + private static async Task?> GetStackOutputs(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { try { @@ -650,7 +675,7 @@ private static async Task WaitForStackCompletion(string stackId) StackName = stackName }; - var response = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var response = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (response.Stacks.Count > 0) { @@ -666,7 +691,7 @@ private static async Task WaitForStackCompletion(string stackId) } catch (Exception ex) { - _logger.LogError(ex, $"Failed to get stack outputs for {stackName}"); + scenarioLogger.LogError(ex, $"Failed to get stack outputs for {stackName}"); return null; } } @@ -674,7 +699,11 @@ private static async Task WaitForStackCompletion(string stackId) /// /// Deletes the CloudFormation stack and waits for confirmation. /// - private static async Task DeleteCloudFormationStack(string stackName) + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task DeleteCloudFormationStack(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { try { @@ -683,10 +712,10 @@ private static async Task DeleteCloudFormationStack(string stackName) StackName = stackName }; - await _amazonCloudFormation.DeleteStackAsync(request); + await cloudFormationClient.DeleteStackAsync(request); Console.WriteLine($"CloudFormation stack '{stackName}' is being deleted. This may take a few minutes."); - bool stackDeleted = await WaitForStackDeletion(stackName); + bool stackDeleted = await WaitForStackDeletion(stackName, cloudFormationClient, scenarioLogger); if (stackDeleted) { @@ -695,13 +724,13 @@ private static async Task DeleteCloudFormationStack(string stackName) } else { - _logger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); + scenarioLogger.LogError($"Failed to delete CloudFormation stack '{stackName}'."); return false; } } catch (Exception ex) { - _logger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); + scenarioLogger.LogError(ex, $"An error occurred while deleting the CloudFormation stack: {stackName}"); return false; } } @@ -709,7 +738,11 @@ private static async Task DeleteCloudFormationStack(string stackName) /// /// Waits for the stack to be deleted. /// - private static async Task WaitForStackDeletion(string stackName) + /// The name of the CloudFormation stack. + /// The CloudFormation client. + /// The logger. + /// True if the stack was deleted successfully. + private static async Task WaitForStackDeletion(string stackName, IAmazonCloudFormation cloudFormationClient, ILogger scenarioLogger) { int retryCount = 0; const int maxRetries = 30; @@ -724,7 +757,7 @@ private static async Task WaitForStackDeletion(string stackName) try { - var describeStacksResponse = await _amazonCloudFormation.DescribeStacksAsync(describeStacksRequest); + var describeStacksResponse = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); if (describeStacksResponse.Stacks.Count == 0 || describeStacksResponse.Stacks[0].StackStatus == StackStatus.DELETE_COMPLETE) @@ -742,7 +775,7 @@ private static async Task WaitForStackDeletion(string stackName) retryCount++; } - _logger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); + scenarioLogger.LogError($"Timed out waiting for CloudFormation stack '{stackName}' to be deleted."); return false; } diff --git a/dotnetv4/IoT/Scenarios/appsettings.json b/dotnetv4/IoT/Scenarios/appsettings.json deleted file mode 100644 index a84bda6ba5b..00000000000 --- a/dotnetv4/IoT/Scenarios/appsettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "AwsIotDataConfig": { - "Profile": "default", - "ServiceURL": "https://data.iot.us-east-1.amazonaws.com/" - }, - - "AwsConfig": { - "Profile": "default", - "Region": "us-east-1" - } -} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index aea78aeb5b8..1a84da3773d 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -1,168 +1,64 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using Amazon.CloudFormation; using Amazon.IoT; using Amazon.IotData; using IoTActions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Moq; using Xunit; -using Xunit.Abstractions; namespace IoTTests; /// -/// Integration tests for the IoT wrapper methods. +/// Integration tests for the AWS IoT Basics scenario. /// -public class IoTIntegrationTests +public class IoTBasicsTests { - private readonly ITestOutputHelper _output; - private readonly IoTWrapper _iotWrapper; - /// - /// Constructor for the test class. + /// Verifies the scenario with an integration test. No errors should be logged. /// - /// ITestOutputHelper object. - public IoTIntegrationTests(ITestOutputHelper output) - { - _output = output; - - // Set up dependency injection for the Amazon service. - var host = Host.CreateDefaultBuilder() - .ConfigureServices((_, services) => - services.AddAWSService() - .AddAWSService() - .AddTransient() - .AddLogging(builder => builder.AddConsole()) - ) - .Build(); - - _iotWrapper = host.Services.GetRequiredService(); - } - - /// - /// Test the IoT wrapper methods by running through the scenario. - /// - /// A Task object. + /// Async task. [Fact] [Trait("Category", "Integration")] - public async Task IoTWrapperMethodsTest() + public async Task TestScenarioIntegration() { - // Set to non-interactive mode for testing + // Arrange IoTBasics.IoTBasics.IsInteractive = false; - - var thingName = $"test-thing-{Guid.NewGuid():N}"; - var certificateArn = ""; - var certificateId = ""; - - try - { - _output.WriteLine("Starting IoT integration test..."); - - // 1. Create an IoT Thing - _output.WriteLine($"Creating IoT Thing: {thingName}"); - var thingArn = await _iotWrapper.CreateThingAsync(thingName); - Assert.False(string.IsNullOrEmpty(thingArn)); - _output.WriteLine($"Created Thing with ARN: {thingArn}"); - - // 2. Create a certificate - _output.WriteLine("Creating device certificate..."); - var certificateResult = await _iotWrapper.CreateKeysAndCertificateAsync(); - Assert.True(certificateResult.HasValue); - var (certArn, certPem, certId) = certificateResult.Value; - certificateArn = certArn; - certificateId = certId; - Assert.False(string.IsNullOrEmpty(certificateArn)); - Assert.False(string.IsNullOrEmpty(certPem)); - Assert.False(string.IsNullOrEmpty(certificateId)); - _output.WriteLine($"Created certificate with ARN: {certificateArn}"); - - // 3. Attach certificate to Thing - _output.WriteLine("Attaching certificate to Thing..."); - var attachResult = await _iotWrapper.AttachThingPrincipalAsync(thingName, certificateArn); - Assert.True(attachResult); - - // 4. Update Thing with attributes - _output.WriteLine("Updating Thing attributes..."); - var attributes = new Dictionary - { - { "TestAttribute", "TestValue" }, - { "Environment", "Testing" } - }; - var updateResult = await _iotWrapper.UpdateThingAsync(thingName, attributes); - Assert.True(updateResult); - - // 5. Get IoT endpoint - _output.WriteLine("Getting IoT endpoint..."); - var endpoint = await _iotWrapper.DescribeEndpointAsync(); - Assert.False(string.IsNullOrEmpty(endpoint)); - _output.WriteLine($"Retrieved endpoint: {endpoint}"); - - // 6. List certificates - _output.WriteLine("Listing certificates..."); - var certificates = await _iotWrapper.ListCertificatesAsync(); - Assert.NotNull(certificates); - Assert.True(certificates.Count > 0); - _output.WriteLine($"Found {certificates.Count} certificates"); - - // 7. Update Thing shadow - _output.WriteLine("Updating Thing shadow..."); - var shadowPayload = """{"state": {"desired": {"temperature": 22, "humidity": 45}}}"""; - var shadowResult = await _iotWrapper.UpdateThingShadowAsync(thingName, shadowPayload); - Assert.True(shadowResult); - - // 8. Get Thing shadow - _output.WriteLine("Getting Thing shadow..."); - var shadowData = await _iotWrapper.GetThingShadowAsync(thingName); - Assert.False(string.IsNullOrEmpty(shadowData)); - _output.WriteLine($"Retrieved shadow data: {shadowData}"); - - // 9. List topic rules - _output.WriteLine("Listing topic rules..."); - var rules = await _iotWrapper.ListTopicRulesAsync(); - Assert.NotNull(rules); - _output.WriteLine($"Found {rules.Count} IoT rules"); - - // 10. Search Things - _output.WriteLine("Searching for Things..."); - var searchResults = await _iotWrapper.SearchIndexAsync($"thingName:{thingName}"); - Assert.NotNull(searchResults); - // Note: Search may not immediately return results for newly created Things - _output.WriteLine($"Search returned {searchResults.Count} results"); - - // 11. List Things - _output.WriteLine("Listing Things..."); - var things = await _iotWrapper.ListThingsAsync(); - Assert.NotNull(things); - Assert.True(things.Count > 0); - _output.WriteLine($"Found {things.Count} Things"); - - _output.WriteLine("IoT integration test completed successfully!"); - } - finally - { - // Cleanup resources - try - { - if (!string.IsNullOrEmpty(certificateArn)) - { - _output.WriteLine("Cleaning up: Detaching certificate from Thing..."); - await _iotWrapper.DetachThingPrincipalAsync(thingName, certificateArn); - - _output.WriteLine("Cleaning up: Deleting certificate..."); - await _iotWrapper.DeleteCertificateAsync(certificateId); - } - _output.WriteLine("Cleaning up: Deleting Thing..."); - await _iotWrapper.DeleteThingAsync(thingName); - - _output.WriteLine("Cleanup completed successfully."); - } - catch (Exception ex) - { - _output.WriteLine($"Warning: Cleanup failed: {ex.Message}"); - } - } + var loggerScenarioMock = new Mock>(); + var loggerWrapperMock = new Mock>(); + + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + // Act + IoTBasics.IoTBasics.logger = loggerScenarioMock.Object; + + // Set up the wrapper and CloudFormation client + IoTBasics.IoTBasics.Wrapper = new IoTWrapper( + new AmazonIoTClient(), + new AmazonIotDataClient("https://dummy-iot-endpoint.amazonaws.com/"), + loggerWrapperMock.Object); + + IoTBasics.IoTBasics.CloudFormationClient = new AmazonCloudFormationClient(); + + await IoTBasics.IoTBasics.RunScenarioAsync(); + + // Assert no errors logged + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == Microsoft.Extensions.Logging.LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); } } diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index ed1b6b951ca..5a9ad27e2d9 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -11,6 +11,7 @@ + From 57e838568a9cf928bd10451aed3e36670c744086 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:09:41 -0600 Subject: [PATCH 18/26] Fix formatting and warnings --- dotnetv4/IoT/Actions/HelloIoT.cs | 4 +-- dotnetv4/IoT/Actions/IoTWrapper.cs | 10 +++---- dotnetv4/IoT/Scenarios/IoTBasics.cs | 34 +++++++++++------------ dotnetv4/IoT/Scenarios/IoTBasics.csproj | 2 +- dotnetv4/IoT/Tests/IoTIntegrationTests.cs | 2 +- dotnetv4/IoT/Tests/IoTTests.csproj | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index de16511f44f..603bb8ea969 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -42,7 +42,7 @@ public static async Task Main(string[] args) Console.WriteLine($" Thing ARN: {thing.ThingArn}"); Console.WriteLine($" Thing Type: {thing.ThingTypeName ?? "No type specified"}"); Console.WriteLine($" Version: {thing.Version}"); - + if (thing.Attributes?.Count > 0) { Console.WriteLine(" Attributes:"); @@ -72,4 +72,4 @@ public static async Task Main(string[] args) } } } -// snippet-end:[iot.dotnetv4.Hello] +// snippet-end:[iot.dotnetv4.Hello] \ No newline at end of file diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index ae9290a475f..7fbb34c06c2 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -209,7 +209,7 @@ public async Task> ListCertificatesAsync() { var request = new ListCertificatesRequest(); var response = await _amazonIoT.ListCertificatesAsync(request); - + _logger.LogInformation($"Retrieved {response.Certificates.Count} certificates"); return response.Certificates; } @@ -278,7 +278,7 @@ public async Task UpdateThingShadowAsync(string thingName, string shadowPa var response = await _amazonIotData.GetThingShadowAsync(request); using var reader = new StreamReader(response.Payload); var shadowData = await reader.ReadToEndAsync(); - + _logger.LogInformation($"Retrieved shadow for Thing {thingName}"); return shadowData; } @@ -357,7 +357,7 @@ public async Task> ListTopicRulesAsync() { var request = new ListTopicRulesRequest(); var response = await _amazonIoT.ListTopicRulesAsync(request); - + _logger.LogInformation($"Retrieved {response.Rules.Count} IoT rules"); return response.Rules; } @@ -607,7 +607,7 @@ public async Task> ListThingsAsync() { var request = new ListThingsRequest(); var response = await _amazonIoT.ListThingsAsync(request); - + _logger.LogInformation($"Retrieved {response.Things.Count} Things"); return response.Things; } @@ -625,4 +625,4 @@ public async Task> ListThingsAsync() // snippet-end:[iot.dotnetv4.ListThings] } -// snippet-end:[iot.dotnetv4.IoTWrapper] +// snippet-end:[iot.dotnetv4.IoTWrapper] \ No newline at end of file diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 305945e7138..43f042c78c1 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -43,13 +43,13 @@ public static async Task Main(string[] args) // Set up dependency injection for the Amazon service. using var host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => - services.AddAWSService(new AWSOptions(){Region = RegionEndpoint.USEast1}) + services.AddAWSService(new AWSOptions() { Region = RegionEndpoint.USEast1 }) .AddAWSService() .AddTransient() .AddLogging(builder => builder.AddConsole()) .AddSingleton(sp => { - var iotService = sp.GetService(); + var iotService = sp.GetRequiredService(); var request = new DescribeEndpointRequest { EndpointType = "iot:Data-ATS" @@ -133,7 +133,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("1. Create an AWS IoT Thing."); Console.WriteLine("An AWS IoT Thing represents a virtual entity in the AWS IoT service that can be associated with a physical device."); Console.WriteLine(); - + if (IsInteractive) { Console.Write("Enter Thing name: "); @@ -145,7 +145,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo { Console.WriteLine($"Using default Thing name: {thingName}"); } - + var thingArn = await iotWrapper.CreateThingAsync(thingName); Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); Console.WriteLine(new string('-', 80)); @@ -155,7 +155,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("2. Generate a device certificate."); Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); Console.WriteLine(); - + var createCert = "y"; if (IsInteractive) { @@ -327,7 +327,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("Creates a rule that is an administrator-level action."); Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); Console.WriteLine(); - + if (IsInteractive) { Console.Write("Enter Rule name: "); @@ -342,7 +342,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Deploy CloudFormation stack to create SNS topic and IAM role Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); - + var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); if (deployStack) { @@ -358,10 +358,10 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo { snsTopicArn = stackOutputs["SNSTopicArn"]; string roleArn = stackOutputs["RoleArn"]; - + Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); - + // Now create the IoT rule with the CloudFormation outputs var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); if (ruleResult) @@ -489,7 +489,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine(new string('-', 80)); Console.WriteLine("13. Clean up CloudFormation stack."); Console.WriteLine("Deleting the CloudFormation stack and all resources..."); - + var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { @@ -513,7 +513,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo catch (Exception ex) { scenarioLogger.LogError(ex, "Error occurred during scenario execution."); - + // Cleanup on error if (!string.IsNullOrEmpty(certificateArn) && !string.IsNullOrEmpty(thingName)) { @@ -527,7 +527,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo scenarioLogger.LogError(cleanupEx, "Error during cleanup."); } } - + if (!string.IsNullOrEmpty(thingName)) { try @@ -552,7 +552,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo scenarioLogger.LogError(cleanupEx, "Error during CloudFormation stack cleanup."); } } - + throw; } } @@ -574,7 +574,7 @@ private static async Task DeployCloudFormationStack(string stackName, IAma { StackName = stackName, TemplateBody = await File.ReadAllTextAsync(_stackResourcePath), - Capabilities = new List{ Capability.CAPABILITY_NAMED_IAM } + Capabilities = new List { Capability.CAPABILITY_NAMED_IAM } }; var response = await cloudFormationClient.CreateStackAsync(request); @@ -676,7 +676,7 @@ private static async Task WaitForStackCompletion(string stackId, IAmazonCl }; var response = await cloudFormationClient.DescribeStacksAsync(describeStacksRequest); - + if (response.Stacks.Count > 0) { var outputs = new Dictionary(); @@ -686,7 +686,7 @@ private static async Task WaitForStackCompletion(string stackId, IAmazonCl } return outputs; } - + return null; } catch (Exception ex) @@ -813,4 +813,4 @@ private static string PromptUserForStackName() return _stackName; } } -// snippet-end:[iot.dotnetv4.IoTScenario] +// snippet-end:[iot.dotnetv4.IoTScenario] \ No newline at end of file diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.csproj b/dotnetv4/IoT/Scenarios/IoTBasics.csproj index 16c59301e29..b5409fe8683 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.csproj +++ b/dotnetv4/IoT/Scenarios/IoTBasics.csproj @@ -12,7 +12,7 @@ - + diff --git a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs index 1a84da3773d..2616e612bb7 100644 --- a/dotnetv4/IoT/Tests/IoTIntegrationTests.cs +++ b/dotnetv4/IoT/Tests/IoTIntegrationTests.cs @@ -61,4 +61,4 @@ public async Task TestScenarioIntegration() It.IsAny>()), Times.Never); } -} +} \ No newline at end of file diff --git a/dotnetv4/IoT/Tests/IoTTests.csproj b/dotnetv4/IoT/Tests/IoTTests.csproj index 5a9ad27e2d9..84cf3d4fb76 100644 --- a/dotnetv4/IoT/Tests/IoTTests.csproj +++ b/dotnetv4/IoT/Tests/IoTTests.csproj @@ -11,7 +11,7 @@ - + From 10c6d7411a1695eda761bd626fc98be8a65807c7 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:24:05 -0600 Subject: [PATCH 19/26] Metadata and readme. --- .doc_gen/metadata/iot_metadata.yaml | 115 ++++++++++++++++++++++++ dotnetv4/IoT/README.md | 131 ++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 dotnetv4/IoT/README.md diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 6339c137a6f..64d72610ffb 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -22,6 +22,14 @@ iot_Hello: - description: snippet_tags: - iot.java2.hello_iot.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.Hello C++: versions: - sdk_version: 1 @@ -55,6 +63,14 @@ iot_DescribeEndpoint: - description: snippet_tags: - iot.java2.describe.endpoint.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DescribeEndpoint Rust: versions: - sdk_version: 1 @@ -75,6 +91,14 @@ iot_DescribeEndpoint: iot: {DescribeEndpoint} iot_ListThings: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListThings Rust: versions: - sdk_version: 1 @@ -104,6 +128,14 @@ iot_ListCertificates: - description: snippet_tags: - iot.java2.list.certs.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.ListCertificates C++: versions: - sdk_version: 1 @@ -133,6 +165,14 @@ iot_CreateKeysAndCertificate: - description: snippet_tags: - iot.java2.create.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateKeysAndCertificate C++: versions: - sdk_version: 1 @@ -162,6 +202,14 @@ iot_DeleteCertificate: - description: snippet_tags: - iot.java2.delete.cert.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteCertificate C++: versions: - sdk_version: 1 @@ -191,6 +239,14 @@ iot_SearchIndex: - description: snippet_tags: - iot.java2.search.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.SearchIndex C++: versions: - sdk_version: 1 @@ -232,6 +288,14 @@ iot_DeleteThing: - description: snippet_tags: - iot.java2.delete.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DeleteThing C++: versions: - sdk_version: 1 @@ -290,6 +354,14 @@ iot_AttachThingPrincipal: - description: snippet_tags: - iot.java2.attach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.AttachThingPrincipal C++: versions: - sdk_version: 1 @@ -319,6 +391,14 @@ iot_DetachThingPrincipal: - description: snippet_tags: - iot.java2.detach.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.DetachThingPrincipal C++: versions: - sdk_version: 1 @@ -348,6 +428,14 @@ iot_UpdateThing: - description: snippet_tags: - iot.java2.update.shadow.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.UpdateThing C++: versions: - sdk_version: 1 @@ -377,6 +465,14 @@ iot_CreateTopicRule: - description: snippet_tags: - iot.java2.create.rule.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateTopicRule C++: versions: - sdk_version: 1 @@ -418,6 +514,14 @@ iot_CreateThing: - description: snippet_tags: - iot.java2.create.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.CreateThing C++: versions: - sdk_version: 1 @@ -464,6 +568,17 @@ iot_Scenario: - description: A wrapper class for &IoT; SDK methods. snippet_tags: - iot.java2.scenario.actions.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: Run an interactive scenario demonstrating &IoT; features. + snippet_tags: + - iot.dotnetv4.IoTScenario + - description: A wrapper class for &IoT; SDK methods. + snippet_tags: + - iot.dotnetv4.IoTWrapper C++: versions: - sdk_version: 1 diff --git a/dotnetv4/IoT/README.md b/dotnetv4/IoT/README.md new file mode 100644 index 00000000000..182d485e154 --- /dev/null +++ b/dotnetv4/IoT/README.md @@ -0,0 +1,131 @@ +# Amazon IoT code examples for the SDK for .NET (v4) + +## Overview + +Shows how to use the AWS SDK for .NET (v4) to work with AWS IoT Core. + + + + +_AWS IoT Core is a managed cloud service that lets connected devices easily and securely interact with cloud applications and other devices._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + + + +### Get started + +- [Hello AWS IoT](Actions/HelloIoT.cs#L20) (`ListThings`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](Scenarios/IoTBasics.cs) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [AttachThingPrincipal](Actions/IoTWrapper.cs#L156) +- [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L84) +- [CreateThing](Actions/IoTWrapper.cs#L34) +- [CreateTopicRule](Actions/IoTWrapper.cs#L336) +- [DeleteCertificate](Actions/IoTWrapper.cs#L556) +- [DeleteThing](Actions/IoTWrapper.cs#L589) +- [DescribeEndpoint](Actions/IoTWrapper.cs#L213) +- [DetachThingPrincipal](Actions/IoTWrapper.cs#L526) +- [GetThingShadow](Actions/IoTWrapper.cs#L312) +- [ListCertificates](Actions/IoTWrapper.cs#L243) +- [ListThings](Actions/IoTWrapper.cs#L614) +- [ListTopicRules](Actions/IoTWrapper.cs#L373) +- [SearchIndex](Actions/IoTWrapper.cs#L402) +- [UpdateThing](Actions/IoTWrapper.cs#L119) +- [UpdateThingShadow](Actions/IoTWrapper.cs#L280) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello AWS IoT + +This example shows you how to get started using AWS IoT Core. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an AWS IoT Thing. +- Generate a device certificate. +- Update an AWS IoT Thing with Attributes. +- Return a unique endpoint specific to the Amazon Web Services account. +- List your AWS IoT certificates. +- Create an AWS IoT shadow that refers to a digital representation or virtual twin of a physical IoT device. +- Write out the state information, in JSON format. +- Creates a rule that is an administrator-level action. +- List your rules. +- Search things using the Thing name. +- Clean up resources. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + + + + + +## Additional resources + +- [AWS IoT Core Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) +- [AWS IoT Core API Reference](https://docs.aws.amazon.com/iot/latest/apireference/Welcome.html) +- [SDK for .NET (v4) AWS IoT reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/IoT/NIoT.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 From fc1ecbe0fbe8f081862508965d4ebbb6897ac628 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:34:20 -0600 Subject: [PATCH 20/26] Update SPECIFICATION.md --- scenarios/basics/iot/SPECIFICATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 6ec42575059..ad3ddf0ca41 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -6,7 +6,7 @@ This example shows how to use AWS SDKs to perform device management use cases us The AWS Iot API provides secure, bi-directional communication between Internet-connected devices (such as sensors, actuators, embedded devices, or smart appliances) and the Amazon Web Services cloud. This example shows some typical use cases such as creating things, creating certifications, applying the certifications to the IoT Thing and so on. ## Resources -This program should create and manage these AWS resources automatically using CloudFormation: +This program should create and manage these AWS resources automatically using CloudFormation and the provided stack .yaml file: 1. **roleARN** - The ARN of an IAM role that has permission to work with AWS IoT. This role is created through CloudFormation stack deployment with proper permissions to publish to SNS topics. 2. **snsAction** - An ARN of an SNS topic. This topic is created through CloudFormation stack deployment for use with IoT rules. From fa37e469d8d03f67c42de6b37a2ba492067f1189 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:26:39 -0600 Subject: [PATCH 21/26] Update for listing and pagination. --- dotnetv4/IoT/Actions/HelloIoT.cs | 2 +- dotnetv4/IoT/Actions/IoTWrapper.cs | 28 ++++++++-- dotnetv4/IoT/Scenarios/IoTBasics.cs | 70 ++++++++++++++++-------- scenarios/basics/iot/SPECIFICATION.md | 77 ++++++++++++++++++--------- 4 files changed, 125 insertions(+), 52 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index 603bb8ea969..ec6a8a5000c 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -23,7 +23,7 @@ public static async Task Main(string[] args) try { - Console.WriteLine("Hello AWS IoT! Let's list your IoT Things:"); + Console.WriteLine("Hello AWS IoT! Let's list a few of your IoT Things:"); Console.WriteLine(new string('-', 80)); var request = new ListThingsRequest diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 7fbb34c06c2..783c8349686 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -605,11 +605,33 @@ public async Task> ListThingsAsync() { try { - var request = new ListThingsRequest(); + // Use pages of 10. + var request = new ListThingsRequest() + { + MaxResults = 10 + }; var response = await _amazonIoT.ListThingsAsync(request); - _logger.LogInformation($"Retrieved {response.Things.Count} Things"); - return response.Things; + // Since there is not a built-in paginator, use the NextMarker to paginate. + bool hasMoreResults = true; + + var things = new List(); + while (hasMoreResults) + { + things.AddRange(response.Things); + + // If NextMarker is not null, there are more results. Get the next page of results. + if (!String.IsNullOrEmpty(response.NextMarker)) + { + request.Marker = response.NextMarker; + response = await _amazonIoT.ListThingsAsync(request); + } + else + hasMoreResults = false; + } + + _logger.LogInformation($"Retrieved {things.Count} Things"); + return things; } catch (Amazon.IoT.Model.ThrottlingException ex) { diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 43f042c78c1..188edcba51e 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -18,7 +18,7 @@ namespace IoTBasics; // snippet-start:[iot.dotnetv4.IoTScenario] /// -/// Scenario class for AWS IoT basics workflow. +/// Scenario class for AWS IoT basics. /// public class IoTBasics { @@ -72,7 +72,7 @@ public static async Task Main(string[] args) _amazonCloudFormation = CloudFormationClient; Console.WriteLine(new string('-', 80)); - Console.WriteLine("Welcome to the AWS IoT example workflow."); + Console.WriteLine("Welcome to the AWS IoT example scenario."); Console.WriteLine("This example program demonstrates various interactions with the AWS Internet of Things (IoT) Core service."); Console.WriteLine(); if (IsInteractive) @@ -93,7 +93,7 @@ public static async Task Main(string[] args) } Console.WriteLine(new string('-', 80)); - Console.WriteLine("The AWS IoT workflow has successfully completed."); + Console.WriteLine("The AWS IoT scenario has successfully completed."); Console.WriteLine(new string('-', 80)); } @@ -123,7 +123,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo string thingName = $"iot-thing-{Guid.NewGuid():N}"; string certificateArn = ""; string certificateId = ""; - string ruleName = $"iot-rule-{Guid.NewGuid():N}"; + string ruleName = $"iotruledefault"; string snsTopicArn = ""; try @@ -150,9 +150,39 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine($"{thingName} was successfully created. The ARN value is {thingArn}"); Console.WriteLine(new string('-', 80)); + // Step 1.1: List AWS IoT Things + Console.WriteLine(new string('-', 80)); + Console.WriteLine("2. List AWS IoT Things."); + Console.WriteLine("Now let's list the IoT Things to see the Thing we just created."); + Console.WriteLine(); + if (IsInteractive) + { + Console.WriteLine("Press Enter to continue..."); + Console.ReadLine(); + } + + var things = await iotWrapper.ListThingsAsync(); + Console.WriteLine($"Found {things.Count} IoT Things:"); + foreach (var thing in things.Take(10)) // Show first 10 things + { + Console.WriteLine($"Thing Name: {thing.ThingName}"); + Console.WriteLine($"Thing ARN: {thing.ThingArn}"); + if (thing.Attributes != null && thing.Attributes.Any()) + { + Console.WriteLine("Attributes:"); + foreach (var attr in thing.Attributes) + { + Console.WriteLine($" {attr.Key}: {attr.Value}"); + } + } + Console.WriteLine("--------------"); + } + Console.WriteLine(); + Console.WriteLine(new string('-', 80)); + // Step 2: Generate a Device Certificate Console.WriteLine(new string('-', 80)); - Console.WriteLine("2. Generate a device certificate."); + Console.WriteLine("3. Generate a device certificate."); Console.WriteLine("A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform."); Console.WriteLine(); @@ -216,7 +246,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 4: Update an AWS IoT Thing with Attributes Console.WriteLine(new string('-', 80)); - Console.WriteLine("3. Update an AWS IoT Thing with Attributes."); + Console.WriteLine("4. Update an AWS IoT Thing with Attributes."); Console.WriteLine("IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data"); Console.WriteLine("management and retrieval within the AWS IoT ecosystem."); Console.WriteLine(); @@ -239,8 +269,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 5: Return a unique endpoint specific to the Amazon Web Services account Console.WriteLine(new string('-', 80)); - Console.WriteLine("4. Return a unique endpoint specific to the Amazon Web Services account."); - Console.WriteLine("An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service."); + Console.WriteLine("5. Return a unique endpoint specific to the Amazon Web Services account."); Console.WriteLine(); if (IsInteractive) { @@ -263,7 +292,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 6: List your AWS IoT certificates Console.WriteLine(new string('-', 80)); - Console.WriteLine("5. List your AWS IoT certificates"); + Console.WriteLine("6. List your AWS IoT certificates"); if (IsInteractive) { Console.WriteLine("Press Enter to continue..."); @@ -281,10 +310,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 7: Create an IoT shadow Console.WriteLine(new string('-', 80)); - Console.WriteLine("6. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); - Console.WriteLine("A Thing Shadow refers to a feature that enables you to create a virtual representation, or \"shadow,\""); - Console.WriteLine("of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between"); - Console.WriteLine("the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow."); + Console.WriteLine("7. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); Console.WriteLine(); if (IsInteractive) { @@ -310,7 +336,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 8: Write out the state information, in JSON format Console.WriteLine(new string('-', 80)); - Console.WriteLine("7. Write out the state information, in JSON format."); + Console.WriteLine("8. Write out the state information, in JSON format."); if (IsInteractive) { Console.WriteLine("Press Enter to continue..."); @@ -323,14 +349,12 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 9: Set up resources (SNS topic and IAM role) and create a rule Console.WriteLine(new string('-', 80)); - Console.WriteLine("8. Set up resources and create a rule"); - Console.WriteLine("Creates a rule that is an administrator-level action."); - Console.WriteLine("Any user who has permission to create rules will be able to access data processed by the rule."); + Console.WriteLine("9. Set up resources and create a rule"); Console.WriteLine(); if (IsInteractive) { - Console.Write("Enter Rule name: "); + Console.Write($"Enter Rule name (press Enter for default '{ruleName}'):: "); var userRuleName = Console.ReadLine(); if (!string.IsNullOrEmpty(userRuleName)) ruleName = userRuleName; @@ -391,7 +415,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 10: List your rules Console.WriteLine(new string('-', 80)); - Console.WriteLine("9. List your rules."); + Console.WriteLine("10. List your rules."); if (IsInteractive) { Console.WriteLine("Press Enter to continue..."); @@ -411,7 +435,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 11: Search things using the Thing name Console.WriteLine(new string('-', 80)); - Console.WriteLine("10. Search things using the Thing name."); + Console.WriteLine("11. Search things using the Thing name."); if (IsInteractive) { Console.WriteLine("Press Enter to continue..."); @@ -446,7 +470,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo if (deleteCert?.ToLower() == "y") { - Console.WriteLine("11. You selected to detach and delete the certificate."); + Console.WriteLine("12. You selected to detach and delete the certificate."); if (IsInteractive) { Console.WriteLine("Press Enter to continue..."); @@ -464,7 +488,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 13: Delete the AWS IoT Thing Console.WriteLine(new string('-', 80)); - Console.WriteLine("12. Delete the AWS IoT Thing."); + Console.WriteLine("13. Delete the AWS IoT Thing."); var deleteThing = "y"; if (IsInteractive) { @@ -487,7 +511,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo if (!string.IsNullOrEmpty(snsTopicArn)) { Console.WriteLine(new string('-', 80)); - Console.WriteLine("13. Clean up CloudFormation stack."); + Console.WriteLine("14. Clean up CloudFormation stack."); Console.WriteLine("Deleting the CloudFormation stack and all resources..."); var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index ad3ddf0ca41..ddbfb6b0b8f 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -21,7 +21,7 @@ The CloudFormation template provides Infrastructure as Code (IaC) benefits, ensu ## Hello AWS IoT -This program is intended for users not familiar with the Amazon IoT SDK to easily get up and running. The logic is to show use of `listThingsPaginator`. +This program is intended for users not familiar with the Amazon IoT SDK to easily get up and running. The logic is to show use of `listThings` for up to 10 things. ## Scenario Program Flow @@ -31,44 +31,49 @@ This scenario demonstrates the following key AWS IoT Service operations: - Use the `CreateThing` API to create a new AWS IoT Thing. - Specify the Thing name and any desired Thing attributes. -2. **Generate a Device Certificate**: +2. **List AWS IoT Things**: + - Use the `ListThings` API to retrieve a list of all AWS IoT Things in the account. + - Display the Thing names, ARNs, and any associated attributes. + - This step demonstrates how to verify that the Thing was created successfully and shows other existing Things. + +3. **Generate a Device Certificate**: - Use the `CreateKeysAndCertificate` API to generate a new device certificate. - The certificate is used to authenticate the device when connecting to AWS IoT. -3. **Attach the Certificate to the AWS IoT Thing**: +4. **Attach the Certificate to the AWS IoT Thing**: - Use the `AttachThingPrincipal` API to associate the device certificate with the AWS IoT Thing. - This allows the device to authenticate and communicate with the AWS IoT Core service. -4. **Update an AWS IoT Thing with Attributes**: +5. **Update an AWS IoT Thing with Attributes**: - Use the `UpdateThingShadow` API to update the Thing's shadow with new attribute values. - The Thing's shadow represents the device's state and properties. -5. **Get an AWS IoT Endpoint**: +6. **Get an AWS IoT Endpoint**: - Use the `DescribeEndpoint` API to retrieve the AWS IoT Core service endpoint. - The device uses this endpoint to connect and communicate with AWS IoT. -6. **List Certificates**: +7. **List Certificates**: - Use the `ListCertificates` API to retrieve a list of all certificates associated with the AWS IoT account. -7. **Detach and Delete the Certificate**: +8. **Detach and Delete the Certificate**: - Use the `DetachThingPrincipal` API to detach the certificate from the AWS IoT Thing. - Use the `DeleteCertificate` API to delete the certificate. -8. **Update the Thing Shadow**: +9. **Update the Thing Shadow**: - Use the `UpdateThingShadow` API to update the Thing's shadow with new state information. - The Thing's shadow represents the device's state and properties. -9. **Write State Information in JSON Format**: - - The state information is written in JSON format, which is the standard data format used by AWS IoT. +10. **Write State Information in JSON Format**: + - The state information is written in JSON format, which is the standard data format used by AWS IoT. -10. **Create an AWS IoT Rule**: +11. **Create an AWS IoT Rule**: - Use the `CreateTopicRule` API to create a new AWS IoT Rule. - Rules allow you to define actions to be performed based on device data or events. -11. **List AWS IoT Rules**: +12. **List AWS IoT Rules**: - Use the `ListTopicRules` API to retrieve a list of all AWS IoT Rules. -12. **Search AWS IoT Things**: +13. **Search AWS IoT Things**: - Use the `SearchIndex` API to search for AWS IoT Things based on various criteria, such as Thing name, attributes, or shadow state. - **Automatic Index Configuration**: The search functionality includes intelligent handling of index setup: - If the search index is not configured, the system automatically detects this condition through exception handling @@ -78,7 +83,7 @@ This scenario demonstrates the following key AWS IoT Service operations: - Validates the indexing configuration status before retrying search operations - Provides detailed logging throughout the index setup process to keep users informed of progress -13. **Delete an AWS IoT Thing**: +14. **Delete an AWS IoT Thing**: - Use the `DeleteThing` API to delete an AWS IoT Thing. Note: We have buy off on these operations from IoT SME. @@ -90,6 +95,7 @@ Each AWS IoT operation can throw specific exceptions that should be handled appr | Action | Error | Handling | |------------------------|---------------------------------|------------------------------------------------------------------------| | **CreateThing** | ResourceAlreadyExistsException | Skip the creation and notify the user +| **ListThings** | ThrottlingException | Notify the user to try again later | **CreateKeysAndCertificate** | ThrottlingException | Notify the user to try again later | **AttachThingPrincipal** | ResourceNotFoundException | Notify cannot perform action and return | **UpdateThing** | ResourceNotFoundException | Notify cannot perform action and return @@ -130,7 +136,27 @@ Enter Thing name: foo5543 foo5543 was successfully created. The ARN value is arn:aws:iot:us-east-1:814548047983:thing/foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -2. Generate a device certificate. +2. List AWS IoT Things. +Now let's list the IoT Things to see the Thing we just created. + +Press Enter to continue... +Found 3 IoT Things: +Thing Name: foo5543 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/foo5543 +-------------- +Thing Name: existing-thing-1 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/existing-thing-1 +Attributes: + Location: Seattle + DeviceType: Sensor +-------------- +Thing Name: existing-thing-2 +Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/existing-thing-2 +-------------- + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +3. Generate a device certificate. A device certificate performs a role in securing the communication between devices (Things) and the AWS IoT platform. Do you want to create a certificate for foo5543? (y/n)y @@ -151,7 +177,7 @@ Thing Name: foo5543 Thing ARN: arn:aws:iot:us-east-1:814548047983:thing/foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -3. Update an AWS IoT Thing with Attributes. +4. Update an AWS IoT Thing with Attributes. IoT Thing attributes, represented as key-value pairs, offer a pivotal advantage in facilitating efficient data management and retrieval within the AWS IoT ecosystem. @@ -159,7 +185,7 @@ Press Enter to continue... Thing attributes updated successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -4. Return a unique endpoint specific to the Amazon Web Services account. +5. Return a unique endpoint specific to the Amazon Web Services account. An IoT Endpoint refers to a specific URL or Uniform Resource Locator that serves as the entry point for communication between IoT devices and the AWS IoT service. Press Enter to continue... @@ -167,7 +193,7 @@ Extracted subdomain: a39q2exsoth3da Full Endpoint URL: https://a39q2exsoth3da-ats.iot.us-east-1.amazonaws.com -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -5. List your AWS IoT certificates +6. List your AWS IoT certificates Press Enter to continue... Cert id: 1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 Cert Arn: arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 @@ -178,7 +204,7 @@ Cert Arn: arn:aws:iot:us-east-1:814548047983:cert/c0d340f1fa8484075d84b523144369 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -6. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device +7. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device A Thing Shadow refers to a feature that enables you to create a virtual representation, or "shadow," of a physical device or thing. The Thing Shadow allows you to synchronize and control the state of a device between the cloud and the device itself. and the AWS IoT service. For example, you can write and retrieve JSON data from a Thing Shadow. @@ -187,12 +213,12 @@ Press Enter to continue... Thing Shadow updated successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -7. Write out the state information, in JSON format. +8. Write out the state information, in JSON format. Press Enter to continue... Received Shadow Data: {"state":{"reported":{"temperature":25,"humidity":50}},"metadata":{"reported":{"temperature":{"timestamp":1707413791},"humidity":{"timestamp":1707413791}}},"version":1,"timestamp":1707413794} -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -8. Creates a rule +9. Creates a rule Creates a rule that is an administrator-level action. Any user who has permission to create rules will be able to access data processed by the rule. @@ -200,7 +226,7 @@ Enter Rule name: rule8823 IoT Rule created successfully. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -9. List your rules. +10. List your rules. Press Enter to continue... List of IoT Rules: Rule Name: rule0099 @@ -217,19 +243,19 @@ Rule ARN: arn:aws:iot:us-east-1:814548047983:rule/YourRuleName11 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -10. Search things using the Thing name. +11. Search things using the Thing name. Press Enter to continue... Thing id found using search is abad8003-3abd-4614-bc04-8d0b6211eb9e -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- Do you want to detach and delete the certificate for foo5543? (y/n)y -11. You selected to detach amd delete the certificate. +12. You selected to detach amd delete the certificate. Press Enter to continue... arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 was successfully removed from foo5543 arn:aws:iot:us-east-1:814548047983:cert/1c9cd9a0f315b58e549e84c38ada37ced24e89047a15ff7ac4abafae9ff6dfc6 was successfully deleted. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -12. Delete the AWS IoT Thing. +13. Delete the AWS IoT Thing. Do you want to delete the IoT Thing? (y/n)y Deleted Thing foo5543 -------------------------------------------------------------------------------- @@ -258,5 +284,6 @@ The following table describes the metadata used in this scenario. | `updateThing` | iot_metadata.yaml | iot_UpdateThing | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | +| `listThings` | iot_metadata.yaml | iot_ListThings | | `hello` | iot_metadata.yaml | iot_Hello | | `scenario | iot_metadata.yaml | iot_Scenario | From fc5f53af6a26528459120b8d2ab93455f4c4b438 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:12:08 -0600 Subject: [PATCH 22/26] Updates to READMEs and spec to clear up confusing metadata. --- .doc_gen/metadata/iot_metadata.yaml | 35 ++++++++++++++++-- cpp/example_code/iot/README.md | 1 + dotnetv4/IoT/README.md | 52 +++++++++++++-------------- dotnetv4/IoT/Scenarios/IoTBasics.cs | 2 +- javav2/example_code/iot/README.md | 2 +- kotlin/services/iot/README.md | 1 + scenarios/basics/iot/SPECIFICATION.md | 3 +- 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 64d72610ffb..097dd50ce45 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -420,6 +420,35 @@ iot_UpdateThing: - description: snippet_tags: - iot.kotlin.update.thing.main + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.UpdateThing + C++: + versions: + - sdk_version: 1 + github: cpp/example_code/iot + excerpts: + - description: + snippet_tags: + - cpp.example_code.iot.UpdateThing + services: + iot: {UpdateThing} +iot_UpdateThingShadow: + languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/iot + sdkguide: + excerpts: + - description: + snippet_tags: + - iot.kotlin.update.shadow.thing.main Java: versions: - sdk_version: 2 @@ -435,7 +464,7 @@ iot_UpdateThing: excerpts: - description: snippet_tags: - - iot.dotnetv4.UpdateThing + - iot.dotnetv4.UpdateThingShadow C++: versions: - sdk_version: 1 @@ -443,9 +472,9 @@ iot_UpdateThing: excerpts: - description: snippet_tags: - - cpp.example_code.iot.UpdateThing + - cpp.example_code.iot.UpdateThingShadow services: - iot: {UpdateThing} + iot: {UpdateThingShadow} iot_CreateTopicRule: languages: Kotlin: diff --git a/cpp/example_code/iot/README.md b/cpp/example_code/iot/README.md index 9497e020860..00ec68d8116 100644 --- a/cpp/example_code/iot/README.md +++ b/cpp/example_code/iot/README.md @@ -67,6 +67,7 @@ Code excerpts that show you how to call individual service functions. - [SearchIndex](search_index.cpp#L22) - [UpdateIndexingConfiguration](update_indexing_configuration.cpp#L22) - [UpdateThing](update_thing.cpp#L23) +- [UpdateThingShadow](update_thing_shadow.cpp#L22) diff --git a/dotnetv4/IoT/README.md b/dotnetv4/IoT/README.md index 182d485e154..1ed4e5872d8 100644 --- a/dotnetv4/IoT/README.md +++ b/dotnetv4/IoT/README.md @@ -1,13 +1,13 @@ -# Amazon IoT code examples for the SDK for .NET (v4) +# AWS IoT code examples for the SDK for .NET (v4) ## Overview -Shows how to use the AWS SDK for .NET (v4) to work with AWS IoT Core. +Shows how to use the AWS SDK for .NET (v4) to work with AWS IoT. -_AWS IoT Core is a managed cloud service that lets connected devices easily and securely interact with cloud applications and other devices._ +_AWS IoT provides secure, bi-directional communication for Internet-connected devices (such as sensors, actuators, embedded devices, wireless devices, and smart appliances) to connect to the AWS Cloud over MQTT, HTTPS, and LoRaWAN._ ## ⚠ Important @@ -31,7 +31,7 @@ For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4 ### Get started -- [Hello AWS IoT](Actions/HelloIoT.cs#L20) (`ListThings`) +- [Hello AWS IoT](Actions/HelloIoT.cs#L9) (`listThings`) ### Basics @@ -45,21 +45,19 @@ Code examples that show you how to perform the essential operations within a ser Code excerpts that show you how to call individual service functions. -- [AttachThingPrincipal](Actions/IoTWrapper.cs#L156) -- [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L84) -- [CreateThing](Actions/IoTWrapper.cs#L34) -- [CreateTopicRule](Actions/IoTWrapper.cs#L336) -- [DeleteCertificate](Actions/IoTWrapper.cs#L556) -- [DeleteThing](Actions/IoTWrapper.cs#L589) -- [DescribeEndpoint](Actions/IoTWrapper.cs#L213) -- [DetachThingPrincipal](Actions/IoTWrapper.cs#L526) -- [GetThingShadow](Actions/IoTWrapper.cs#L312) -- [ListCertificates](Actions/IoTWrapper.cs#L243) -- [ListThings](Actions/IoTWrapper.cs#L614) -- [ListTopicRules](Actions/IoTWrapper.cs#L373) -- [SearchIndex](Actions/IoTWrapper.cs#L402) -- [UpdateThing](Actions/IoTWrapper.cs#L119) -- [UpdateThingShadow](Actions/IoTWrapper.cs#L280) +- [AttachThingPrincipal](Actions/IoTWrapper.cs#L98) +- [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L67) +- [CreateThing](Actions/IoTWrapper.cs#L35) +- [CreateTopicRule](Actions/IoTWrapper.cs#L298) +- [DeleteCertificate](Actions/IoTWrapper.cs#L526) +- [DeleteThing](Actions/IoTWrapper.cs#L567) +- [DescribeEndpoint](Actions/IoTWrapper.cs#L170) +- [DetachThingPrincipal](Actions/IoTWrapper.cs#L492) +- [ListCertificates](Actions/IoTWrapper.cs#L201) +- [ListThings](Actions/IoTWrapper.cs#L599) +- [SearchIndex](Actions/IoTWrapper.cs#L377) +- [UpdateThing](Actions/IoTWrapper.cs#L132) +- [UpdateThingShadow](Actions/IoTWrapper.cs#L229) @@ -75,7 +73,7 @@ Code excerpts that show you how to call individual service functions. #### Hello AWS IoT -This example shows you how to get started using AWS IoT Core. +This example shows you how to get started using AWS IoT. #### Learn the basics @@ -85,14 +83,14 @@ This example shows you how to do the following: - Create an AWS IoT Thing. - Generate a device certificate. - Update an AWS IoT Thing with Attributes. -- Return a unique endpoint specific to the Amazon Web Services account. +- Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow that refers to a digital representation or virtual twin of a physical IoT device. -- Write out the state information, in JSON format. -- Creates a rule that is an administrator-level action. +- Create an AWS IoT shadow. +- Write out state information. +- Creates a rule. - List your rules. - Search things using the Thing name. -- Clean up resources. +- Delete an AWS IoT Thing. @@ -117,8 +115,8 @@ in the `dotnetv4` folder. ## Additional resources -- [AWS IoT Core Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) -- [AWS IoT Core API Reference](https://docs.aws.amazon.com/iot/latest/apireference/Welcome.html) +- [AWS IoT Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) +- [AWS IoT API Reference](https://docs.aws.amazon.com/iot/latest/apireference/Welcome.html) - [SDK for .NET (v4) AWS IoT reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/IoT/NIoT.html) diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 188edcba51e..895ed5f4576 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -310,7 +310,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo // Step 7: Create an IoT shadow Console.WriteLine(new string('-', 80)); - Console.WriteLine("7. Create an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); + Console.WriteLine("7. Update an IoT shadow that refers to a digital representation or virtual twin of a physical IoT device"); Console.WriteLine(); if (IsInteractive) { diff --git a/javav2/example_code/iot/README.md b/javav2/example_code/iot/README.md index 2c5c80544ba..6e3dde8c82d 100644 --- a/javav2/example_code/iot/README.md +++ b/javav2/example_code/iot/README.md @@ -56,7 +56,7 @@ Code excerpts that show you how to call individual service functions. - [DetachThingPrincipal](src/main/java/com/example/iot/scenario/IotActions.java#L572) - [ListCertificates](src/main/java/com/example/iot/scenario/IotActions.java#L368) - [SearchIndex](src/main/java/com/example/iot/scenario/IotActions.java#L531) -- [UpdateThing](src/main/java/com/example/iot/scenario/IotActions.java#L267) +- [UpdateThingShadow](src/main/java/com/example/iot/scenario/IotActions.java#L267) diff --git a/kotlin/services/iot/README.md b/kotlin/services/iot/README.md index 58a4fb91b24..5558f9186b2 100644 --- a/kotlin/services/iot/README.md +++ b/kotlin/services/iot/README.md @@ -57,6 +57,7 @@ Code excerpts that show you how to call individual service functions. - [ListCertificates](src/main/kotlin/com/example/iot/IotScenario.kt#L384) - [SearchIndex](src/main/kotlin/com/example/iot/IotScenario.kt#L295) - [UpdateThing](src/main/kotlin/com/example/iot/IotScenario.kt#L429) +- [UpdateThingShadow](src/main/kotlin/com/example/iot/IotScenario.kt#L456) diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index ddbfb6b0b8f..a4ab2f13cf2 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -282,8 +282,9 @@ The following table describes the metadata used in this scenario. | `attachThingPrincipal` | iot_metadata.yaml | iot_AttachThingPrincipal | | `detachThingPrincipal` | iot_metadata.yaml | iot_DetachThingPrincipal | | `updateThing` | iot_metadata.yaml | iot_UpdateThing | +| `updateThingShadow` | iot_metadata.yaml | iot_UpdateThingShadow | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | | `listThings` | iot_metadata.yaml | iot_ListThings | | `hello` | iot_metadata.yaml | iot_Hello | -| `scenario | iot_metadata.yaml | iot_Scenario | +| `scenario` | iot_metadata.yaml | iot_Scenario | From 6f0162da443c5cbd9b077dc33b28e51b22644cbd Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:50:18 -0600 Subject: [PATCH 23/26] Update spec and printouts and READMEs. --- .doc_gen/metadata/iot-data_metadata.yaml | 16 ++++++++++ .doc_gen/metadata/iot_metadata.yaml | 39 +---------------------- cpp/example_code/iot/README.md | 3 +- dotnetv4/IoT/Actions/IoTWrapper.cs | 32 +++++++++++++++++++ dotnetv4/IoT/README.md | 13 ++++---- dotnetv4/IoT/Scenarios/IoTBasics.cs | 40 +++++++++++++++--------- javav2/example_code/iot/README.md | 3 +- kotlin/services/iot/README.md | 3 +- scenarios/basics/iot/SPECIFICATION.md | 18 +++++++++-- 9 files changed, 100 insertions(+), 67 deletions(-) diff --git a/.doc_gen/metadata/iot-data_metadata.yaml b/.doc_gen/metadata/iot-data_metadata.yaml index f8469e86bcd..5343e895e05 100644 --- a/.doc_gen/metadata/iot-data_metadata.yaml +++ b/.doc_gen/metadata/iot-data_metadata.yaml @@ -1,6 +1,14 @@ # zexi 0.4.0 iot-data-plane_GetThingShadow: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.GetThingShadow Kotlin: versions: - sdk_version: 1 @@ -30,6 +38,14 @@ iot-data-plane_GetThingShadow: iot-data-plane: {GetThingShadow} iot-data-plane_UpdateThingShadow: languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/IoT + excerpts: + - description: + snippet_tags: + - iot.dotnetv4.UpdateThingShadow Kotlin: versions: - sdk_version: 1 diff --git a/.doc_gen/metadata/iot_metadata.yaml b/.doc_gen/metadata/iot_metadata.yaml index 097dd50ce45..ba89253bd68 100644 --- a/.doc_gen/metadata/iot_metadata.yaml +++ b/.doc_gen/metadata/iot_metadata.yaml @@ -438,43 +438,6 @@ iot_UpdateThing: - cpp.example_code.iot.UpdateThing services: iot: {UpdateThing} -iot_UpdateThingShadow: - languages: - Kotlin: - versions: - - sdk_version: 1 - github: kotlin/services/iot - sdkguide: - excerpts: - - description: - snippet_tags: - - iot.kotlin.update.shadow.thing.main - Java: - versions: - - sdk_version: 2 - github: javav2/example_code/iot - excerpts: - - description: - snippet_tags: - - iot.java2.update.shadow.thing.main - .NET: - versions: - - sdk_version: 4 - github: dotnetv4/IoT - excerpts: - - description: - snippet_tags: - - iot.dotnetv4.UpdateThingShadow - C++: - versions: - - sdk_version: 1 - github: cpp/example_code/iot - excerpts: - - description: - snippet_tags: - - cpp.example_code.iot.UpdateThingShadow - services: - iot: {UpdateThingShadow} iot_CreateTopicRule: languages: Kotlin: @@ -568,7 +531,7 @@ iot_Scenario: - Update an &IoT; Thing with Attributes. - Return a unique endpoint. - List your &IoT; certificates. - - Create an &IoT; shadow. + - Update an &IoT; shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/cpp/example_code/iot/README.md b/cpp/example_code/iot/README.md index 00ec68d8116..efdfa9986ae 100644 --- a/cpp/example_code/iot/README.md +++ b/cpp/example_code/iot/README.md @@ -67,7 +67,6 @@ Code excerpts that show you how to call individual service functions. - [SearchIndex](search_index.cpp#L22) - [UpdateIndexingConfiguration](update_indexing_configuration.cpp#L22) - [UpdateThing](update_thing.cpp#L23) -- [UpdateThingShadow](update_thing_shadow.cpp#L22) @@ -107,7 +106,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/dotnetv4/IoT/Actions/IoTWrapper.cs b/dotnetv4/IoT/Actions/IoTWrapper.cs index 783c8349686..2fe0ee8f90f 100644 --- a/dotnetv4/IoT/Actions/IoTWrapper.cs +++ b/dotnetv4/IoT/Actions/IoTWrapper.cs @@ -346,6 +346,38 @@ public async Task CreateTopicRuleAsync(string ruleName, string snsTopicArn } // snippet-end:[iot.dotnetv4.CreateTopicRule] + // snippet-start:[iot.dotnetv4.DeleteTopicRule] + /// + /// Deletes an IoT topic rule. + /// + /// The name of the rule. + /// True if successful, false otherwise. + public async Task DeleteTopicRuleAsync(string ruleName) + { + try + { + var request = new DeleteTopicRuleRequest + { + RuleName = ruleName, + }; + + await _amazonIoT.DeleteTopicRuleAsync(request); + _logger.LogInformation($"Deleted IoT rule {ruleName}"); + return true; + } + catch (Amazon.IoT.Model.ResourceNotFoundException ex) + { + _logger.LogWarning($"Rule {ruleName} not found: {ex.Message}"); + return false; + } + catch (Exception ex) + { + _logger.LogError($"Couldn't delete topic rule. Here's why: {ex.Message}"); + return false; + } + } + // snippet-end:[iot.dotnetv4.DeleteTopicRule] + // snippet-start:[iot.dotnetv4.ListTopicRules] /// /// Lists all IoT topic rules. diff --git a/dotnetv4/IoT/README.md b/dotnetv4/IoT/README.md index 1ed4e5872d8..5e24ef44442 100644 --- a/dotnetv4/IoT/README.md +++ b/dotnetv4/IoT/README.md @@ -49,15 +49,14 @@ Code excerpts that show you how to call individual service functions. - [CreateKeysAndCertificate](Actions/IoTWrapper.cs#L67) - [CreateThing](Actions/IoTWrapper.cs#L35) - [CreateTopicRule](Actions/IoTWrapper.cs#L298) -- [DeleteCertificate](Actions/IoTWrapper.cs#L526) -- [DeleteThing](Actions/IoTWrapper.cs#L567) +- [DeleteCertificate](Actions/IoTWrapper.cs#L558) +- [DeleteThing](Actions/IoTWrapper.cs#L599) - [DescribeEndpoint](Actions/IoTWrapper.cs#L170) -- [DetachThingPrincipal](Actions/IoTWrapper.cs#L492) +- [DetachThingPrincipal](Actions/IoTWrapper.cs#L524) - [ListCertificates](Actions/IoTWrapper.cs#L201) -- [ListThings](Actions/IoTWrapper.cs#L599) -- [SearchIndex](Actions/IoTWrapper.cs#L377) +- [ListThings](Actions/IoTWrapper.cs#L631) +- [SearchIndex](Actions/IoTWrapper.cs#L409) - [UpdateThing](Actions/IoTWrapper.cs#L132) -- [UpdateThingShadow](Actions/IoTWrapper.cs#L229) @@ -85,7 +84,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/dotnetv4/IoT/Scenarios/IoTBasics.cs b/dotnetv4/IoT/Scenarios/IoTBasics.cs index 895ed5f4576..1ca03e6dce6 100644 --- a/dotnetv4/IoT/Scenarios/IoTBasics.cs +++ b/dotnetv4/IoT/Scenarios/IoTBasics.cs @@ -352,24 +352,21 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine("9. Set up resources and create a rule"); Console.WriteLine(); - if (IsInteractive) - { - Console.Write($"Enter Rule name (press Enter for default '{ruleName}'):: "); - var userRuleName = Console.ReadLine(); - if (!string.IsNullOrEmpty(userRuleName)) - ruleName = userRuleName; - } - else - { - Console.WriteLine($"Using default rule name: {ruleName}"); - } - // Deploy CloudFormation stack to create SNS topic and IAM role Console.WriteLine("Deploying CloudFormation stack to create SNS topic and IAM role..."); var deployStack = !IsInteractive || GetYesNoResponse("Would you like to deploy the CloudFormation stack? (y/n) "); if (deployStack) { + if (IsInteractive) + { + Console.Write( + $"Enter stack resource file path (or press Enter for default '{_stackResourcePath}'): "); + var userResourcePath = Console.ReadLine(); + if (!string.IsNullOrEmpty(userResourcePath)) + _stackResourcePath = userResourcePath; + } + _stackName = PromptUserForStackName(); var deploySuccess = await DeployCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); @@ -386,6 +383,18 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo Console.WriteLine($"Successfully deployed stack. SNS topic: {snsTopicArn}"); Console.WriteLine($"Successfully deployed stack. IAM role: {roleArn}"); + if (IsInteractive) + { + Console.Write($"Enter Rule name (press Enter for default '{ruleName}'): "); + var userRuleName = Console.ReadLine(); + if (!string.IsNullOrEmpty(userRuleName)) + ruleName = userRuleName; + } + else + { + Console.WriteLine($"Using default rule name: {ruleName}"); + } + // Now create the IoT rule with the CloudFormation outputs var ruleResult = await iotWrapper.CreateTopicRuleAsync(ruleName, snsTopicArn, roleArn); if (ruleResult) @@ -517,8 +526,10 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo var cleanup = !IsInteractive || GetYesNoResponse("Do you want to delete the CloudFormation stack and all resources? (y/n) "); if (cleanup) { - var cleanupSuccess = await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); - if (cleanupSuccess) + var ruleCleanupSuccess = await iotWrapper.DeleteTopicRuleAsync(ruleName); + + var stackCleanupSuccess = await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); + if (ruleCleanupSuccess && stackCleanupSuccess) { Console.WriteLine("Successfully cleaned up CloudFormation stack and all resources."); } @@ -569,6 +580,7 @@ private static async Task RunScenarioInternalAsync(IoTWrapper iotWrapper, IAmazo { try { + await _iotWrapper.DeleteTopicRuleAsync(ruleName); await DeleteCloudFormationStack(_stackName, cloudFormationClient, scenarioLogger); } catch (Exception cleanupEx) diff --git a/javav2/example_code/iot/README.md b/javav2/example_code/iot/README.md index 6e3dde8c82d..fe4953a53a7 100644 --- a/javav2/example_code/iot/README.md +++ b/javav2/example_code/iot/README.md @@ -56,7 +56,6 @@ Code excerpts that show you how to call individual service functions. - [DetachThingPrincipal](src/main/java/com/example/iot/scenario/IotActions.java#L572) - [ListCertificates](src/main/java/com/example/iot/scenario/IotActions.java#L368) - [SearchIndex](src/main/java/com/example/iot/scenario/IotActions.java#L531) -- [UpdateThingShadow](src/main/java/com/example/iot/scenario/IotActions.java#L267) @@ -84,7 +83,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/kotlin/services/iot/README.md b/kotlin/services/iot/README.md index 5558f9186b2..1c69a063d74 100644 --- a/kotlin/services/iot/README.md +++ b/kotlin/services/iot/README.md @@ -57,7 +57,6 @@ Code excerpts that show you how to call individual service functions. - [ListCertificates](src/main/kotlin/com/example/iot/IotScenario.kt#L384) - [SearchIndex](src/main/kotlin/com/example/iot/IotScenario.kt#L295) - [UpdateThing](src/main/kotlin/com/example/iot/IotScenario.kt#L429) -- [UpdateThingShadow](src/main/kotlin/com/example/iot/IotScenario.kt#L456) @@ -85,7 +84,7 @@ This example shows you how to do the following: - Update an AWS IoT Thing with Attributes. - Return a unique endpoint. - List your AWS IoT certificates. -- Create an AWS IoT shadow. +- Update an AWS IoT shadow. - Write out state information. - Creates a rule. - List your rules. diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index a4ab2f13cf2..4d8f21e2e94 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -64,7 +64,7 @@ This scenario demonstrates the following key AWS IoT Service operations: - The Thing's shadow represents the device's state and properties. 10. **Write State Information in JSON Format**: - - The state information is written in JSON format, which is the standard data format used by AWS IoT. + - Use the 'GetThingShadow' to get the state information. The state information is written in JSON format, which is the standard data format used by AWS IoT. 11. **Create an AWS IoT Rule**: - Use the `CreateTopicRule` API to create a new AWS IoT Rule. @@ -86,7 +86,10 @@ This scenario demonstrates the following key AWS IoT Service operations: 14. **Delete an AWS IoT Thing**: - Use the `DeleteThing` API to delete an AWS IoT Thing. - Note: We have buy off on these operations from IoT SME. + +15. **Clean up resources**: + - Clean up the stack and rule. + ## Exception Handling @@ -260,6 +263,17 @@ Do you want to delete the IoT Thing? (y/n)y Deleted Thing foo5543 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- +14. Clean up CloudFormation stack. +Deleting the CloudFormation stack and all resources... +CloudFormation stack 'IoTBasicsStack' is being deleted. This may take a few minutes. +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +Waiting for CloudFormation stack 'IoTBasicsStack' to be deleted... +CloudFormation stack 'IoTBasicsStack' has been deleted. +Successfully cleaned up CloudFormation stack and all resources. +-------------------------------------------------------------------------------- The AWS IoT workflow has successfully completed. -------------------------------------------------------------------------------- ``` From 8d11f4f9e893c97b310afd2cda16a9fbd52f79fa Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:10:00 -0600 Subject: [PATCH 24/26] Cleanup Hello example. --- dotnetv4/IoT/Actions/HelloIoT.cs | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/dotnetv4/IoT/Actions/HelloIoT.cs b/dotnetv4/IoT/Actions/HelloIoT.cs index ec6a8a5000c..7f9cfd02f53 100644 --- a/dotnetv4/IoT/Actions/HelloIoT.cs +++ b/dotnetv4/IoT/Actions/HelloIoT.cs @@ -23,20 +23,38 @@ public static async Task Main(string[] args) try { - Console.WriteLine("Hello AWS IoT! Let's list a few of your IoT Things:"); + Console.WriteLine("Hello AWS IoT! Let's list your IoT Things:"); Console.WriteLine(new string('-', 80)); - var request = new ListThingsRequest + // Use pages of 10. + var request = new ListThingsRequest() { MaxResults = 10 }; - var response = await iotClient.ListThingsAsync(request); - if (response.Things is { Count: > 0 }) + // Since there is not a built-in paginator, use the NextMarker to paginate. + bool hasMoreResults = true; + + var things = new List(); + while (hasMoreResults) + { + things.AddRange(response.Things); + + // If NextMarker is not null, there are more results. Get the next page of results. + if (!String.IsNullOrEmpty(response.NextMarker)) + { + request.Marker = response.NextMarker; + response = await iotClient.ListThingsAsync(request); + } + else + hasMoreResults = false; + } + + if (things is { Count: > 0 }) { - Console.WriteLine($"Found {response.Things.Count} IoT Things:"); - foreach (var thing in response.Things) + Console.WriteLine($"Found {things.Count} IoT Things:"); + foreach (var thing in things) { Console.WriteLine($"- Thing Name: {thing.ThingName}"); Console.WriteLine($" Thing ARN: {thing.ThingArn}"); @@ -62,13 +80,13 @@ public static async Task Main(string[] args) Console.WriteLine("Hello IoT completed successfully."); } - catch (Exception ex) + catch (Amazon.IoT.Model.ThrottlingException ex) { - Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine($"Request throttled, please try again later: {ex.Message}"); } - finally + catch (Exception ex) { - iotClient.Dispose(); + Console.WriteLine($"Couldn't list Things. Here's why: {ex.Message}"); } } } From b14244d1babc2b2a523aecdbf981e973f54d8872 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:18:48 -0600 Subject: [PATCH 25/26] Add to main solution. --- dotnetv4/DotNetV4Examples.sln | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln index 820afe0cfbe..c3ce0a7e71a 100644 --- a/dotnetv4/DotNetV4Examples.sln +++ b/dotnetv4/DotNetV4Examples.sln @@ -153,6 +153,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedshiftBasics", "Redshift\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedshiftTests", "Redshift\Tests\RedshiftTests.csproj", "{1DB37AC5-18FE-4535-816C-827B4D3DCB96}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IoT", "IoT", "{AF6DCE90-C605-41AE-A37C-770E2D654E9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTTests", "IoT\Tests\IoTTests.csproj", "{D25392A3-A6F2-6BA8-9478-B47D192BC2D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTBasics", "IoT\Scenarios\IoTBasics.csproj", "{D739A0A7-8A33-1CC5-31AC-15B2497BD130}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IoTActions", "IoT\Actions\IoTActions.csproj", "{0FA755B3-F5B4-F551-504E-5EE86DBF1719}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -775,6 +783,42 @@ Global {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x64.Build.0 = Release|Any CPU {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x86.ActiveCfg = Release|Any CPU {1DB37AC5-18FE-4535-816C-827B4D3DCB96}.Release|x86.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x64.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Debug|x86.Build.0 = Debug|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|Any CPU.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x64.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x64.Build.0 = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x86.ActiveCfg = Release|Any CPU + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1}.Release|x86.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x64.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x64.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x86.ActiveCfg = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Debug|x86.Build.0 = Debug|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|Any CPU.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x64.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x64.Build.0 = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x86.ActiveCfg = Release|Any CPU + {D739A0A7-8A33-1CC5-31AC-15B2497BD130}.Release|x86.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x64.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x64.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x86.ActiveCfg = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Debug|x86.Build.0 = Debug|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|Any CPU.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x64.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x64.Build.0 = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x86.ActiveCfg = Release|Any CPU + {0FA755B3-F5B4-F551-504E-5EE86DBF1719}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -843,6 +887,9 @@ Global {4E74F2DB-3BA5-4390-8FBF-C58F57601671} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} {A30F8E57-AF18-40EC-B130-140585246CC7} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} {1DB37AC5-18FE-4535-816C-827B4D3DCB96} = {BC1690DE-FD9E-72EA-CAED-A2B9A3D6B335} + {D25392A3-A6F2-6BA8-9478-B47D192BC2D1} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} + {D739A0A7-8A33-1CC5-31AC-15B2497BD130} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} + {0FA755B3-F5B4-F551-504E-5EE86DBF1719} = {AF6DCE90-C605-41AE-A37C-770E2D654E9F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} From 13ecb70fc9e35834169cf8aa0ef3bb4c289bab94 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:26:16 -0600 Subject: [PATCH 26/26] Fixing metadata locations in spec. --- scenarios/basics/iot/SPECIFICATION.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scenarios/basics/iot/SPECIFICATION.md b/scenarios/basics/iot/SPECIFICATION.md index 4d8f21e2e94..2a8c0cc8a58 100644 --- a/scenarios/basics/iot/SPECIFICATION.md +++ b/scenarios/basics/iot/SPECIFICATION.md @@ -288,7 +288,7 @@ The following table describes the metadata used in this scenario. | `describeEndpoint` | iot_metadata.yaml | iot_DescribeEndpoint | | `listThings` | iot_metadata.yaml | iot_ListThings | | `listCertificates` | iot_metadata.yaml | iot_ListCertificates | -| `CreateKeysAndCertificate` | iot_metadata.yaml | iot_CreateKeysAndCertificate | +| `createKeysAndCertificate` | iot_metadata.yaml | iot_CreateKeysAndCertificate | | `deleteCertificate` | iot_metadata.yaml | iot_DeleteCertificate | | `searchIndex` | iot_metadata.yaml | iot_SearchIndex | | `deleteThing` | iot_metadata.yaml | iot_DeleteThing | @@ -296,7 +296,8 @@ The following table describes the metadata used in this scenario. | `attachThingPrincipal` | iot_metadata.yaml | iot_AttachThingPrincipal | | `detachThingPrincipal` | iot_metadata.yaml | iot_DetachThingPrincipal | | `updateThing` | iot_metadata.yaml | iot_UpdateThing | -| `updateThingShadow` | iot_metadata.yaml | iot_UpdateThingShadow | +| `updateThingShadow` | iot-data_metadata.yaml | io-data-plane_UpdateThingShadow | +| `getThingShadow` | iot-data_metadata.yaml | io-data-plane_GetThingShadow | | `createTopicRule` | iot_metadata.yaml | iot_CreateTopicRule | | `createThing` | iot_metadata.yaml | iot_CreateThing | | `listThings` | iot_metadata.yaml | iot_ListThings |