diff --git a/.doc_gen/metadata/s3_metadata.yaml b/.doc_gen/metadata/s3_metadata.yaml
index 462f07881e0..b3e0532a61e 100644
--- a/.doc_gen/metadata/s3_metadata.yaml
+++ b/.doc_gen/metadata/s3_metadata.yaml
@@ -2776,6 +2776,12 @@ s3_Scenario_PresignedUrl:
- description: Generate a presigned URL and perform an upload using that URL.
snippet_tags:
- S3.dotnetv3.UploadUsingPresignedURLExample
+ - sdk_version: 4
+ github: dotnetv4/S3/Scenarios/S3_CreatePresignedPost
+ excerpts:
+ - description: Create and use presigned POST URLs for direct browser uploads.
+ snippet_tags:
+ - S3.dotnetv4.CreatePresignedPostBasics
Java:
versions:
- sdk_version: 2
@@ -3705,3 +3711,17 @@ s3_Scenario_DoesBucketExist:
- s3.java2.does-bucket-exist-main
services:
s3: {GetBucketAcl}
+
+s3_CreatePresignedPost:
+ languages:
+ .NET:
+ versions:
+ - sdk_version: 4
+ github: dotnetv4/S3
+ excerpts:
+ - description: Create a presigned POST URL.
+ genai: most
+ snippet_tags:
+ - S3.dotnetv4.Scenario_CreatePresignedPostAsync
+ services:
+ s3: {CreatePresignedPost}
diff --git a/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs b/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs
index 1ee5a0bf9ea..e29cb38778f 100644
--- a/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs
+++ b/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs
@@ -28,7 +28,8 @@ private static async Task Invoke(string modelId, string prompt)
default:
Console.WriteLine($"Unknown model ID: {modelId}. Valid model IDs are: {CLAUDE}.");
break;
- };
+ }
+ ;
}
}
}
\ No newline at end of file
diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln
index 9c3a74c6cc2..8b243f70231 100644
--- a/dotnetv4/DotNetV4Examples.sln
+++ b/dotnetv4/DotNetV4Examples.sln
@@ -149,7 +149,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "DynamoDB\Scenario
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBActions", "DynamoDB\Actions\DynamoDBActions.csproj", "{B0F91FE2-6AC5-4FA8-B321-54623A516D4D}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "S3", "S3", "{F929DB74-DD0E-B0EF-AA66-D8703D547BBD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Tests", "S3\Tests\S3Tests.csproj", "{11497EB7-B702-B537-3CBE-BA2F4F85F313}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{A65C33EA-4F2E-DE85-7501-4389A2100813}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "S3\Scenarios\S3_CreatePresignedPost\Basics.csproj", "{2B6F24A0-4569-E8A2-81B4-3925FA4F0320}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -365,6 +371,14 @@ Global
{B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -425,6 +439,7 @@ Global
{3F159C49-3DE7-42F5-AF14-E64C03AF19E8} = {EE6D1933-1E38-406A-B691-446326310D1F}
{D44D50E1-EC65-4A1C-AAA1-C360E4FC563F} = {EE6D1933-1E38-406A-B691-446326310D1F}
{7485EAED-F81C-4119-BABC-E009A21ACE46} = {EE6D1933-1E38-406A-B691-446326310D1F}
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
{43C5E98B-5EC4-9F2B-2676-8F1E34969855} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6} = {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232}
@@ -432,7 +447,9 @@ Global
{F578CA07-E74F-4F47-9203-C67777D9BB78} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
{E10920BB-6409-41BB-9A9D-813BC37CC3C0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{B0F91FE2-6AC5-4FA8-B321-54623A516D4D} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
- {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
+ {11497EB7-B702-B537-3CBE-BA2F4F85F313} = {F929DB74-DD0E-B0EF-AA66-D8703D547BBD}
+ {A65C33EA-4F2E-DE85-7501-4389A2100813} = {F929DB74-DD0E-B0EF-AA66-D8703D547BBD}
+ {2B6F24A0-4569-E8A2-81B4-3925FA4F0320} = {A65C33EA-4F2E-DE85-7501-4389A2100813}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA}
diff --git a/dotnetv4/S3/README.md b/dotnetv4/S3/README.md
new file mode 100644
index 00000000000..cf1e626503e
--- /dev/null
+++ b/dotnetv4/S3/README.md
@@ -0,0 +1,97 @@
+# Amazon S3 code examples for the SDK for .NET (v4)
+
+## Overview
+
+Shows how to use the AWS SDK for .NET (v4) to work with Amazon Simple Storage Service (Amazon S3).
+
+
+
+
+_Amazon S3 is storage for the internet. You can use Amazon S3 to store and retrieve any amount of data at any time, from anywhere on the web._
+
+## ⚠ 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.
+
+
+
+
+
+### Single actions
+
+Code excerpts that show you how to call individual service functions.
+
+- [CreatePresignedPost](Scenarios/S3_CreatePresignedPost/S3Wrapper.cs#L35)
+
+### Scenarios
+
+Code examples that show you how to accomplish a specific task by calling multiple
+functions within the same service.
+
+- [Create a presigned URL](Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs)
+
+
+
+
+
+## Run the examples
+
+### Instructions
+
+
+
+
+
+
+
+#### Create a presigned URL
+
+This example shows you how to create a presigned URL for Amazon S3 and upload an object.
+
+
+
+
+
+
+
+
+
+### 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
+
+- [Amazon S3 User Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html)
+- [Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html)
+- [SDK for .NET (v4) Amazon S3 reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/S3/NS3.html)
+
+
+
+
+---
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0
diff --git a/dotnetv4/S3/S3Examples.sln b/dotnetv4/S3/S3Examples.sln
new file mode 100644
index 00000000000..10c6bc5d832
--- /dev/null
+++ b/dotnetv4/S3/S3Examples.sln
@@ -0,0 +1,38 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33414.496
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{C13DDD1A-438D-4E52-90FB-A496A54516C7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C51625C8-3B42-4810-BF1B-0E3C6C716FA6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "Scenarios\S3_CreatePresignedPost\Basics.csproj", "{22C217CC-E2D9-9F79-EE15-24F9D262655E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Tests", "Tests\S3Tests.csproj", "{FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {22C217CC-E2D9-9F79-EE15-24F9D262655E} = {C13DDD1A-438D-4E52-90FB-A496A54516C7}
+ {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C} = {C51625C8-3B42-4810-BF1B-0E3C6C716FA6}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {5ACB5B08-E8F8-453C-B63B-6C0C9DE67780}
+ EndGlobalSection
+EndGlobal
diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj
new file mode 100644
index 00000000000..75694d45011
--- /dev/null
+++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs
new file mode 100644
index 00000000000..5d8d70ead1e
--- /dev/null
+++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs
@@ -0,0 +1,296 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace S3Scenarios;
+
+// snippet-start:[S3.dotnetv4.CreatePresignedPostBasics]
+///
+/// Scenario demonstrating the complete workflow for presigned POST URLs:
+/// 1. Create an S3 bucket
+/// 2. Create a presigned POST URL
+/// 3. Upload a file using the presigned POST URL
+/// 4. Clean up resources
+///
+public class CreatePresignedPostBasics
+{
+ public static ILogger _logger = null!;
+ public static S3Wrapper _s3Wrapper = null!;
+ public static UiMethods _uiMethods = null!;
+ public static IHttpClientFactory _httpClientFactory = null!;
+ public static bool _isInteractive = true;
+ public static string? _bucketName;
+ public static string? _objectKey;
+
+ ///
+ /// Set up the services and logging.
+ ///
+ /// The IHost instance.
+ public static void SetUpServices(IHost host)
+ {
+ var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ });
+ _logger = new Logger(loggerFactory);
+
+ _s3Wrapper = host.Services.GetRequiredService();
+ _httpClientFactory = host.Services.GetRequiredService();
+ _uiMethods = new UiMethods();
+ }
+
+ ///
+ /// Perform the actions defined for the Amazon S3 Presigned POST scenario.
+ ///
+ /// Command line arguments.
+ /// A Task object.
+ public static async Task Main(string[] args)
+ {
+ _isInteractive = !args.Contains("--non-interactive");
+
+ // Set up dependency injection for Amazon S3
+ using var host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
+ .ConfigureServices((_, services) =>
+ services.AddAWSService()
+ .AddTransient()
+ .AddHttpClient()
+ )
+ .Build();
+
+ SetUpServices(host);
+
+ try
+ {
+ // Display overview
+ _uiMethods.DisplayOverview();
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 1: Create bucket
+ await CreateBucketAsync();
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 2: Create presigned URL
+ _uiMethods.DisplayTitle("Step 2: Create presigned POST URL");
+ var response = await CreatePresignedPostAsync();
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 3: Display URL and fields
+ _uiMethods.DisplayTitle("Step 3: Presigned POST URL details");
+ DisplayPresignedPostFields(response);
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 4: Upload file
+ _uiMethods.DisplayTitle("Step 4: Upload test file using presigned POST URL");
+ await UploadFileAsync(response);
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 5: Verify file exists
+ await VerifyFileExistsAsync();
+ _uiMethods.PressEnter(_isInteractive);
+
+ // Step 6: Cleanup
+ _uiMethods.DisplayTitle("Step 6: Clean up resources");
+ await CleanupAsync();
+
+ _uiMethods.DisplayTitle("S3 Presigned POST Scenario completed successfully!");
+ _uiMethods.PressEnter(_isInteractive);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error in scenario");
+ Console.WriteLine($"Error: {ex.Message}");
+
+ // Attempt cleanup if there was an error
+ if (!string.IsNullOrEmpty(_bucketName))
+ {
+ _uiMethods.DisplayTitle("Cleaning up resources after error");
+ await _s3Wrapper.DeleteBucketAsync(_bucketName);
+ Console.WriteLine($"Cleaned up bucket: {_bucketName}");
+ }
+ }
+ }
+
+ ///
+ /// Create an S3 bucket for the scenario.
+ ///
+ private static async Task CreateBucketAsync()
+ {
+ _uiMethods.DisplayTitle("Step 1: Create an S3 bucket");
+
+ // Generate a default bucket name for the scenario
+ var defaultBucketName = $"presigned-post-demo-{DateTime.Now:yyyyMMddHHmmss}".ToLower();
+
+ // Prompt user for bucket name or use default in non-interactive mode
+ _bucketName = _uiMethods.GetUserInput(
+ $"Enter S3 bucket name (or press Enter for '{defaultBucketName}'): ",
+ defaultBucketName,
+ _isInteractive);
+
+ // Basic validation to ensure bucket name is not empty
+ if (string.IsNullOrWhiteSpace(_bucketName))
+ {
+ _bucketName = defaultBucketName;
+ }
+
+ Console.WriteLine($"Creating bucket: {_bucketName}");
+
+ await _s3Wrapper.CreateBucketAsync(_bucketName);
+
+ Console.WriteLine($"Successfully created bucket: {_bucketName}");
+ }
+
+
+ ///
+ /// Create a presigned POST URL.
+ ///
+ private static async Task CreatePresignedPostAsync()
+ {
+ _objectKey = "example-upload.txt";
+ var expiration = DateTime.UtcNow.AddMinutes(10); // Short expiration for the demo
+
+ Console.WriteLine($"Creating presigned POST URL for {_bucketName}/{_objectKey}");
+ Console.WriteLine($"Expiration: {expiration} UTC");
+
+ var s3Client = _s3Wrapper.GetS3Client();
+
+ var response = await _s3Wrapper.CreatePresignedPostAsync(
+ s3Client, _bucketName!, _objectKey, expiration);
+
+ Console.WriteLine("Successfully created presigned POST URL");
+ return response;
+ }
+
+ ///
+ /// Upload a file using the presigned POST URL.
+ ///
+ private static async Task UploadFileAsync(CreatePresignedPostResponse response)
+ {
+
+ // Create a temporary test file to upload
+ string testFilePath = Path.GetTempFileName();
+ string testContent = "This is a test file for the S3 presigned POST scenario.";
+
+ await File.WriteAllTextAsync(testFilePath, testContent);
+ Console.WriteLine($"Created test file at: {testFilePath}");
+
+ // Upload the file using the presigned POST URL
+ Console.WriteLine("\nUploading file using the presigned POST URL...");
+ var uploadResult = await UploadFileWithPresignedPostAsync(response, testFilePath);
+
+ // Display the upload result
+ if (uploadResult.Success)
+ {
+ Console.WriteLine($"Upload successful! Status code: {uploadResult.StatusCode}");
+ }
+ else
+ {
+ Console.WriteLine($"Upload failed with status code: {uploadResult.StatusCode}");
+ Console.WriteLine($"Error: {uploadResult.Response}");
+ throw new Exception("File upload failed");
+ }
+
+ // Clean up the temporary file
+ File.Delete(testFilePath);
+ Console.WriteLine("Temporary file deleted");
+ }
+
+ ///
+ /// Helper method to upload a file using a presigned POST URL.
+ ///
+ private static async Task<(bool Success, HttpStatusCode StatusCode, string Response)> UploadFileWithPresignedPostAsync(
+ CreatePresignedPostResponse response,
+ string filePath)
+ {
+ try
+ {
+ _logger.LogInformation("Uploading file {filePath} using presigned POST URL", filePath);
+
+ using var httpClient = _httpClientFactory.CreateClient();
+ using var formContent = new MultipartFormDataContent();
+
+ // Add all the fields from the presigned POST response
+ foreach (var field in response.Fields)
+ {
+ formContent.Add(new StringContent(field.Value), field.Key);
+ }
+
+ // Add the file content
+ var fileStream = File.OpenRead(filePath);
+ var fileName = Path.GetFileName(filePath);
+ var fileContent = new StreamContent(fileStream);
+ fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
+ formContent.Add(fileContent, "file", fileName);
+
+ // Send the POST request
+ var httpResponse = await httpClient.PostAsync(response.Url, formContent);
+ var responseContent = await httpResponse.Content.ReadAsStringAsync();
+
+ // Log and return the result
+ _logger.LogInformation("Upload completed with status code {statusCode}", httpResponse.StatusCode);
+
+ return (httpResponse.IsSuccessStatusCode, httpResponse.StatusCode, responseContent);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error uploading file");
+ return (false, HttpStatusCode.InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Verify that the uploaded file exists in the S3 bucket.
+ ///
+ private static async Task VerifyFileExistsAsync()
+ {
+ _uiMethods.DisplayTitle("Step 5: Verify uploaded file exists");
+
+ Console.WriteLine($"Checking if file exists at {_bucketName}/{_objectKey}...");
+
+ try
+ {
+ var metadata = await _s3Wrapper.GetObjectMetadataAsync(_bucketName!, _objectKey!);
+
+ Console.WriteLine($"File verification successful! File exists in the bucket.");
+ Console.WriteLine($"File size: {metadata.ContentLength} bytes");
+ Console.WriteLine($"File type: {metadata.Headers.ContentType}");
+ Console.WriteLine($"Last modified: {metadata.LastModified}");
+ }
+ catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ Console.WriteLine($"Error: File was not found in the bucket.");
+ throw;
+ }
+ }
+
+ private static void DisplayPresignedPostFields(CreatePresignedPostResponse response)
+ {
+ Console.WriteLine($"Presigned POST URL: {response.Url}");
+ Console.WriteLine("Form fields to include:");
+
+ foreach (var field in response.Fields)
+ {
+ Console.WriteLine($" {field.Key}: {field.Value}");
+ }
+ }
+
+ ///
+ /// Clean up resources created by the scenario.
+ ///
+ private static async Task CleanupAsync()
+ {
+ if (!string.IsNullOrEmpty(_bucketName))
+ {
+ Console.WriteLine($"Deleting bucket {_bucketName} and its contents...");
+ bool result = await _s3Wrapper.DeleteBucketAsync(_bucketName);
+
+ if (result)
+ {
+ Console.WriteLine("Bucket deleted successfully");
+ }
+ else
+ {
+ Console.WriteLine("Failed to delete bucket - it may have been already deleted");
+ }
+ }
+ }
+}
+// snippet-end:[S3.dotnetv4.CreatePresignedPostBasics]
\ No newline at end of file
diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs
new file mode 100644
index 00000000000..12e629ed827
--- /dev/null
+++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs
@@ -0,0 +1,174 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace S3Scenarios;
+
+///
+/// Wrapper methods for common Amazon Simple Storage Service (Amazon S3)
+/// operations.
+///
+public class S3Wrapper
+{
+ private readonly IAmazonS3 _s3Client;
+ private readonly ILogger _logger;
+
+ ///
+ /// Constructor for the wrapper class.
+ ///
+ /// The injected S3 client.
+ /// The injected logger for use with this class.
+ public S3Wrapper(IAmazonS3 s3Client, ILogger logger)
+ {
+ _s3Client = s3Client;
+ _logger = logger;
+ }
+
+ ///
+ /// Get the Amazon S3 client.
+ ///
+ /// The Amazon S3 client.
+ public IAmazonS3 GetS3Client()
+ {
+ return _s3Client;
+ }
+
+ // snippet-start:[S3.dotnetv4.Scenario_CreatePresignedPostAsync]
+ ///
+ /// Create a presigned POST URL with conditions.
+ ///
+ /// The Amazon S3 client.
+ /// The name of the bucket.
+ /// The object key (path) where the uploaded file will be stored.
+ /// When the presigned URL expires.
+ /// Dictionary of fields to add to the form.
+ /// List of conditions to apply.
+ /// A CreatePresignedPostResponse object with URL and form fields.
+ public async Task CreatePresignedPostAsync(
+ IAmazonS3 s3Client,
+ string bucketName,
+ string objectKey,
+ DateTime expires,
+ Dictionary? fields = null,
+ List? conditions = null)
+ {
+ var request = new CreatePresignedPostRequest
+ {
+ BucketName = bucketName,
+ Key = objectKey,
+ Expires = expires
+ };
+
+ // Add custom fields if provided
+ if (fields != null)
+ {
+ foreach (var field in fields)
+ {
+ request.Fields.Add(field.Key, field.Value);
+ }
+ }
+
+ // Add conditions if provided
+ if (conditions != null)
+ {
+ foreach (var condition in conditions)
+ {
+ request.Conditions.Add(condition);
+ }
+ }
+
+ return await s3Client.CreatePresignedPostAsync(request);
+ }
+ // snippet-end:[S3.dotnetv4.Scenario_CreatePresignedPostAsync]
+
+ ///
+ /// Create a bucket and wait until it's ready to use.
+ ///
+ /// The name of the bucket to create.
+ /// The name of the newly created bucket.
+ public async Task CreateBucketAsync(string bucketName)
+ {
+ _logger.LogInformation("Creating bucket {bucket}", bucketName);
+
+ var request = new PutBucketRequest
+ {
+ BucketName = bucketName
+ };
+
+ var response = await _s3Client.PutBucketAsync(request);
+
+ _logger.LogInformation("Created bucket {bucket} with status {status}",
+ bucketName, response.HttpStatusCode);
+
+ // Wait for the bucket to be available
+ var exist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, bucketName);
+
+ if (!exist)
+ {
+ _logger.LogInformation("Waiting for bucket {bucket} to be ready", bucketName);
+
+ while (!exist)
+ {
+ await Task.Delay(2000);
+ exist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, bucketName);
+ }
+ }
+
+ return bucketName;
+ }
+
+ ///
+ /// Delete an object from an S3 bucket.
+ ///
+ /// The name of the bucket.
+ /// The object key to delete.
+ /// The response from the DeleteObjectAsync call.
+ public async Task DeleteObjectAsync(
+ string bucketName, string objectKey)
+ {
+ var request = new DeleteObjectRequest
+ {
+ BucketName = bucketName,
+ Key = objectKey
+ };
+
+ return await _s3Client.DeleteObjectAsync(request);
+ }
+
+ ///
+ /// Delete an S3 bucket and all its objects.
+ ///
+ /// The name of the bucket to delete.
+ /// A boolean value indicating the success of the operation.
+ public async Task DeleteBucketAsync(string bucketName)
+ {
+ try
+ {
+ // Delete all objects in the bucket
+ await AmazonS3Util.DeleteS3BucketWithObjectsAsync(_s3Client, bucketName);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting bucket {bucket}", bucketName);
+ return false;
+ }
+ }
+
+ ///
+ /// Get object metadata.
+ ///
+ /// The name of the bucket.
+ /// The object key.
+ /// Object metadata.
+ public async Task GetObjectMetadataAsync(
+ string bucketName, string objectKey)
+ {
+ var request = new GetObjectMetadataRequest
+ {
+ BucketName = bucketName,
+ Key = objectKey
+ };
+
+ return await _s3Client.GetObjectMetadataAsync(request);
+ }
+}
\ No newline at end of file
diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs
new file mode 100644
index 00000000000..fdedb6b30d5
--- /dev/null
+++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs
@@ -0,0 +1,72 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace S3Scenarios;
+
+///
+/// UI helper methods for the S3 presigned POST scenario.
+///
+public class UiMethods
+{
+ public readonly string SepBar = new string('-', 88);
+
+ ///
+ /// Show information about the scenario.
+ ///
+ public void DisplayOverview()
+ {
+ DisplayTitle("Welcome to the Amazon S3 Presigned POST URL Scenario");
+
+ Console.WriteLine("This example application does the following:");
+ Console.WriteLine("\t 1. Creates an S3 bucket with a unique name");
+ Console.WriteLine("\t 2. Creates a presigned POST URL for the bucket");
+ Console.WriteLine("\t 3. Displays the URL and form fields needed for browser uploads");
+ Console.WriteLine("\t 4. Uploads a test file using the presigned POST URL");
+ Console.WriteLine("\t 5. Verifies the file was successfully uploaded to S3");
+ Console.WriteLine("\t 6. Cleans up the resources (bucket and test file)");
+ }
+
+ ///
+ /// Display a message and wait until the user presses enter if in interactive mode.
+ ///
+ public void PressEnter(bool interactive)
+ {
+ Console.Write("\nPlease press to continue. ");
+ if (interactive)
+ _ = Console.ReadLine();
+ }
+
+ ///
+ /// Display a line of hyphens, the text of the title and another
+ /// line of hyphens.
+ ///
+ /// The string to be displayed.
+ public void DisplayTitle(string strTitle)
+ {
+ Console.WriteLine(SepBar);
+ Console.WriteLine(strTitle);
+ Console.WriteLine(SepBar);
+ }
+
+ ///
+ /// Get user input with an optional default value.
+ ///
+ /// The prompt to display to the user.
+ /// The default value to use if user doesn't provide input.
+ /// Whether to wait for user input or use default.
+ /// The user input or default value.
+ public string GetUserInput(string prompt, string? defaultValue = null, bool isInteractive = true)
+ {
+ Console.Write(prompt);
+ if (isInteractive)
+ {
+ var input = Console.ReadLine();
+ return string.IsNullOrWhiteSpace(input) ? defaultValue ?? "" : input.Trim();
+ }
+ else
+ {
+ Console.WriteLine(defaultValue ?? "");
+ return defaultValue ?? "";
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs
new file mode 100644
index 00000000000..f3e8a913cc9
--- /dev/null
+++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs
@@ -0,0 +1,11 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using System.Net;
+global using System.Net.Http.Headers;
+global using Amazon.S3;
+global using Amazon.S3.Model;
+global using Amazon.S3.Util;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
\ No newline at end of file
diff --git a/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs b/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs
new file mode 100644
index 00000000000..2c44f6ff5d7
--- /dev/null
+++ b/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs
@@ -0,0 +1,77 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace S3Tests;
+
+///
+/// Integration tests for the Amazon Simple Storage Service (Amazon S3)
+/// presigned POST URL scenario (CreatePresignedPostBasics).
+///
+public class CreatePresignedPostBasicsTests
+{
+ private AmazonS3Client _client = null!;
+ private S3Wrapper _s3Wrapper = null!;
+
+ ///
+ /// Verifies the presigned POST URL scenario with an integration test. No errors should be logged.
+ ///
+ /// Async task.
+ [Fact]
+ [Trait("Category", "Integration")]
+ public async Task TestScenario()
+ {
+ // Arrange.
+ var loggerScenarioMock = new Mock>();
+ var loggerWrapperMock = new Mock>();
+ var uiMethods = new S3Scenarios.UiMethods();
+
+ _client = new AmazonS3Client();
+ _s3Wrapper = new S3Wrapper(_client, loggerWrapperMock.Object);
+
+ // Set up the static fields directly
+ S3Scenarios.CreatePresignedPostBasics._logger = loggerScenarioMock.Object;
+ S3Scenarios.CreatePresignedPostBasics._s3Wrapper = _s3Wrapper;
+ S3Scenarios.CreatePresignedPostBasics._uiMethods = uiMethods;
+ S3Scenarios.CreatePresignedPostBasics._isInteractive = false;
+
+ // Set up verification for error logging.
+ loggerScenarioMock.Setup(logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()
+ ));
+
+ loggerWrapperMock.Setup(logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()
+ ));
+
+ // Act.
+ // Call the static Main method with --non-interactive flag to match previous behavior
+ await S3Scenarios.CreatePresignedPostBasics.Main(new string[] { "--non-interactive" });
+
+ // Assert no exceptions or errors logged.
+ loggerScenarioMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+
+ loggerWrapperMock.Verify(
+ logger => logger.Log(
+ It.Is(logLevel => logLevel == LogLevel.Error),
+ It.IsAny(),
+ It.Is((@object, @type) => true),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Never);
+ }
+}
\ No newline at end of file
diff --git a/dotnetv4/S3/Tests/S3Tests.csproj b/dotnetv4/S3/Tests/S3Tests.csproj
new file mode 100644
index 00000000000..81cfde53213
--- /dev/null
+++ b/dotnetv4/S3/Tests/S3Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/dotnetv4/S3/Tests/Usings.cs b/dotnetv4/S3/Tests/Usings.cs
new file mode 100644
index 00000000000..7fa523be4c4
--- /dev/null
+++ b/dotnetv4/S3/Tests/Usings.cs
@@ -0,0 +1,8 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using Amazon.S3;
+global using Microsoft.Extensions.Logging;
+global using Moq;
+global using S3Scenarios;
+global using Xunit;
\ No newline at end of file