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