diff --git a/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml b/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml
index 3e3e8bd33a1..ab3d20c23e8 100644
--- a/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml
+++ b/.doc_gen/cross-content/cross_FSA_JavaScript_block.xml
@@ -22,7 +22,7 @@
&CMP; determines the sentiment of the extracted text and its language.
- The extracted text is translated to French using &TSL;.
+ The extracted text is translated to English using &TSL;.
&POL; synthesizes an audio file from the extracted text.
diff --git a/.doc_gen/cross-content/cross_FSA_Java_block.xml b/.doc_gen/cross-content/cross_FSA_Java_block.xml
index be14e657e61..5cc9641b3ef 100644
--- a/.doc_gen/cross-content/cross_FSA_Java_block.xml
+++ b/.doc_gen/cross-content/cross_FSA_Java_block.xml
@@ -20,7 +20,7 @@
&CMP; determines the sentiment of the extracted text and its language.
- The extracted text is translated to French using &TSL;.
+ The extracted text is translated to English using &TSL;.
&POL; synthesizes an audio file from the extracted text.
diff --git a/.doc_gen/cross-content/cross_FSA_NetV3_block.xml b/.doc_gen/cross-content/cross_FSA_NetV3_block.xml
new file mode 100644
index 00000000000..e778df46c48
--- /dev/null
+++ b/.doc_gen/cross-content/cross_FSA_NetV3_block.xml
@@ -0,0 +1,33 @@
+
+
+ %phrases-shared;
+]>
+
+
+ This example application analyzes and stores customer feedback cards. Specifically,
+ it fulfills the need of a fictitious hotel in New York City. The hotel receives feedback
+ from guests in various languages in the form of physical comment cards. That feedback
+ is uploaded into the app through a web client.
+
+ After an image of a comment card is uploaded, the following steps occur:
+
+
+
+ Text is extracted from the image using &TEXTRACT;.
+
+
+ &CMP; determines the sentiment of the extracted text and its language.
+
+
+ The extracted text is translated to English using &TSL;.
+
+
+ &POL; synthesizes an audio file from the extracted text.
+
+
+ The full app can be deployed with the &CDK;. For source code and deployment
+ instructions, see the project in
+ GitHub.
+
\ No newline at end of file
diff --git a/.doc_gen/cross-content/cross_FSA_Ruby_block.xml b/.doc_gen/cross-content/cross_FSA_Ruby_block.xml
index 9d734724753..540e39a122f 100644
--- a/.doc_gen/cross-content/cross_FSA_Ruby_block.xml
+++ b/.doc_gen/cross-content/cross_FSA_Ruby_block.xml
@@ -20,7 +20,7 @@
&CMP; determines the sentiment of the extracted text and its language.
- The extracted text is translated to French using &TSL;.
+ The extracted text is translated to English using &TSL;.
&POL; synthesizes an audio file from the extracted text.
diff --git a/.doc_gen/metadata/cross_metadata.yaml b/.doc_gen/metadata/cross_metadata.yaml
index db23246abda..91c4b67f8fb 100644
--- a/.doc_gen/metadata/cross_metadata.yaml
+++ b/.doc_gen/metadata/cross_metadata.yaml
@@ -6,6 +6,10 @@ cross_FSA:
their original language, determines their sentiment, and generates an audio file
from the translated text.
languages:
+ .NET:
+ versions:
+ - sdk_version: 3
+ block_content: cross_FSA_NetV3_block.xml
Java:
versions:
- sdk_version: 2
diff --git a/.github/pre_validate/pre_validate.py b/.github/pre_validate/pre_validate.py
index 07153a109f5..77eb6d7af82 100644
--- a/.github/pre_validate/pre_validate.py
+++ b/.github/pre_validate/pre_validate.py
@@ -202,6 +202,7 @@
'role/AmazonSageMakerGeospatialFullAccess',
'VectorEnrichmentJobDataSourceConfigInput',
'com/workdocs/latest/APIReference/Welcome',
+ 'service/FeedbackSentimentAnalyzer/README'
}
def check_files(root, quiet):
diff --git a/applications/feedback_sentiment_analyzer/README.md b/applications/feedback_sentiment_analyzer/README.md
index 78e0c3ae171..53ad0180580 100644
--- a/applications/feedback_sentiment_analyzer/README.md
+++ b/applications/feedback_sentiment_analyzer/README.md
@@ -17,6 +17,7 @@ This application has been implemented with the following AWS SDKs.
- [Ruby](../../ruby/cross_service_examples/feedback_sentiment_analyzer/README.md)
- [JavaScript](../../javascriptv3/example_code/cross-services/feedback-sentiment-analyzer/README.md)
- [Java](../../javav2/usecases/creating_fsa_app/README.md)
+- [.NET](../../dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md)
To deploy one of these implementations, follow the [Deployment instructions](#deployment-instructions).
diff --git a/applications/feedback_sentiment_analyzer/SPECIFICATION.md b/applications/feedback_sentiment_analyzer/SPECIFICATION.md
index e1e2b787cee..eed56cba22d 100644
--- a/applications/feedback_sentiment_analyzer/SPECIFICATION.md
+++ b/applications/feedback_sentiment_analyzer/SPECIFICATION.md
@@ -285,7 +285,7 @@ For example:
### SynthesizeAudio
-Uses the Amazon Polly [SynthesizeSpeech](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) method to convert input text into life-like speech. Store the synthesized audio in the provided Amazon S3 bucket with a content type of "audio/mp3".
+Uses the Amazon Polly [SynthesizeSpeech](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) method to convert input text into life-like speech. Store the synthesized audio in the provided Amazon S3 bucket with a content type of "audio/mpeg".
#### **Input**
diff --git a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts
index 77bd6a54cf4..a326fa69c02 100644
--- a/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts
+++ b/applications/feedback_sentiment_analyzer/cdk/lib/functions.ts
@@ -214,11 +214,119 @@ const JAVA_FUNCTIONS: AppFunctionConfig[] = [
},
];
+const DOTNET_FUNCTIONS = [
+ {
+ ...BASE_APP_FUNCTION,
+ name: "ExtractText",
+ handler: "FsaExtractText::FsaExtractText.ExtractTextFunction::FunctionHandler",
+ runtime: Runtime.DOTNET_6,
+ codeAsset() {
+ const source = resolve(
+ "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer"
+ );
+ return Code.fromAsset(source, {
+ bundling: {
+ command: [
+ "/bin/sh",
+ "-c",
+ " dotnet tool install -g Amazon.Lambda.Tools" +
+ " && dotnet build" +
+ " && cd FsaExtractText" +
+ " && dotnet lambda package --output-package /asset-output/function.zip",
+ ],
+ image: Runtime.DOTNET_6.bundlingImage,
+ user: "root",
+ outputType: BundlingOutput.ARCHIVED,
+ },
+ });
+ },
+ },
+ {
+ ...BASE_APP_FUNCTION,
+ name: "AnalyzeSentiment",
+ handler: "FsaAnalyzeSentiment::FsaAnalyzeSentiment.AnalyzeSentimentFunction::FunctionHandler",
+ runtime: Runtime.DOTNET_6,
+ codeAsset() {
+ const source = resolve(
+ "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer"
+ );
+ return Code.fromAsset(source, {
+ bundling: {
+ command: [
+ "/bin/sh",
+ "-c",
+ " dotnet tool install -g Amazon.Lambda.Tools" +
+ " && dotnet build" +
+ " && cd FsaAnalyzeSentiment" +
+ " && dotnet lambda package --output-package /asset-output/function.zip",
+ ],
+ image: Runtime.DOTNET_6.bundlingImage,
+ user: "root",
+ outputType: BundlingOutput.ARCHIVED,
+ },
+ });
+ },
+ },
+ {
+ ...BASE_APP_FUNCTION,
+ name: "TranslateText",
+ handler: "FsaTranslateText::FsaTranslateText.TranslateTextFunction::FunctionHandler",
+ runtime: Runtime.DOTNET_6,
+ codeAsset() {
+ const source = resolve(
+ "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer"
+ );
+ return Code.fromAsset(source, {
+ bundling: {
+ command: [
+ "/bin/sh",
+ "-c",
+ " dotnet tool install -g Amazon.Lambda.Tools" +
+ " && dotnet build" +
+ " && cd FsaTranslateText" +
+ " && dotnet lambda package --output-package /asset-output/function.zip",
+ ],
+ image: Runtime.DOTNET_6.bundlingImage,
+ user: "root",
+ outputType: BundlingOutput.ARCHIVED,
+ },
+ });
+ },
+ },
+ {
+ ...BASE_APP_FUNCTION,
+ name: "SynthesizeAudio",
+ handler: "FsaSynthesizeAudio::FsaSynthesizeAudio.SynthesizeAudioFunction::FunctionHandler",
+ runtime: Runtime.DOTNET_6,
+ codeAsset() {
+ const source = resolve(
+ "../../../dotnetv3/cross-service/FeedbackSentimentAnalyzer"
+ );
+ return Code.fromAsset(source, {
+ bundling: {
+ command: [
+ "/bin/sh",
+ "-c",
+ " dotnet tool install -g Amazon.Lambda.Tools" +
+ " && dotnet build" +
+ " && cd FsaSynthesizeAudio" +
+ " && dotnet lambda package --output-package /asset-output/function.zip",
+ ],
+ image: Runtime.DOTNET_6.bundlingImage,
+ user: "root",
+ outputType: BundlingOutput.ARCHIVED,
+ },
+ });
+ },
+ },
+];
+
const FUNCTIONS: Record = {
examplelang: EXAMPLE_LANG_FUNCTIONS,
ruby: RUBY_FUNCTIONS,
java: JAVA_FUNCTIONS,
javascript: JAVASCRIPT_FUNCTIONS,
+ dotnet: DOTNET_FUNCTIONS,
};
export function getFunctions(language: string = ""): AppFunctionConfig[] {
return FUNCTIONS[language] ?? FUNCTIONS.examplelang;
diff --git a/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts b/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts
index eca72660e61..8d6aeba2175 100644
--- a/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts
+++ b/applications/feedback_sentiment_analyzer/cdk/lib/stack.ts
@@ -103,6 +103,7 @@ export class AppStack extends Stack {
"image/png",
"binary/octet-stream",
"audio/mp3",
+ "audio/mpeg",
],
cloudWatchRole: true,
deployOptions: {
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln
new file mode 100644
index 00000000000..64b38fcadfc
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FeedbackSentimentAnalyzer.sln
@@ -0,0 +1,55 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33927.249
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaExtractText", "FsaExtractText\FsaExtractText.csproj", "{C0BB9701-7EE8-4A91-B1EB-B3888ED43079}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaAnalyzeSentiment", "FsaAnalyzeSentiment\FsaAnalyzeSentiment.csproj", "{9B31C723-D4B0-4953-B5B1-9002E7181DEB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaTranslateText", "FsaTranslateText\FsaTranslateText.csproj", "{9886D38A-541E-4BF5-83CE-6CA85088028B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsaSynthesizeAudio", "FsaSynthesizeAudio\FsaSynthesizeAudio.csproj", "{4613C012-D517-4C57-98A3-F59B727889EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsaServices", "FsaServices\FsaServices.csproj", "{DA3393CC-DBBF-444E-947C-FBDC4BA66C55}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsaServicesTest", "FsaServicesTest\FsaServicesTest.csproj", "{3A3503C4-9077-4BB3-90C9-08A2B8ED798D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0BB9701-7EE8-4A91-B1EB-B3888ED43079}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B31C723-D4B0-4953-B5B1-9002E7181DEB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9886D38A-541E-4BF5-83CE-6CA85088028B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9886D38A-541E-4BF5-83CE-6CA85088028B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9886D38A-541E-4BF5-83CE-6CA85088028B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9886D38A-541E-4BF5-83CE-6CA85088028B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4613C012-D517-4C57-98A3-F59B727889EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4613C012-D517-4C57-98A3-F59B727889EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4613C012-D517-4C57-98A3-F59B727889EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4613C012-D517-4C57-98A3-F59B727889EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA3393CC-DBBF-444E-947C-FBDC4BA66C55}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A3503C4-9077-4BB3-90C9-08A2B8ED798D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {C7E557C4-6C58-4B38-8C84-D71A72BD3F3C}
+ EndGlobalSection
+EndGlobal
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs
new file mode 100644
index 00000000000..e80b999ae5c
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/AnalyzeSentimentFunction.cs
@@ -0,0 +1,55 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Comprehend;
+using Amazon.Lambda.Core;
+using AWS.Lambda.Powertools.Logging;
+using FsaServices.Models;
+using FsaServices.Services;
+
+// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
+[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
+
+namespace FsaAnalyzeSentiment;
+
+///
+/// Function to handle analysis of sentiment.
+///
+public class AnalyzeSentimentFunction
+{
+ private readonly SentimentService _sentimentService;
+
+ ///
+ /// Default constructor. This constructor is used by AWS Lambda to construct the instance. When invoked in a Lambda environment,
+ /// the AWS credentials will come from the AWS Identity and Access Management (IAM) role associated with the function. The AWS Region will be set to the
+ /// Region the Lambda function is executed in.
+ ///
+ public AnalyzeSentimentFunction()
+ {
+ _sentimentService = new SentimentService(new AmazonComprehendClient());
+ }
+
+ ///
+ /// Constructs an instance with a preconfigured Comprehend client. This can be used for testing outside of the Lambda environment.
+ ///
+ ///
+ public AnalyzeSentimentFunction(IAmazonComprehend comprehendClient)
+ {
+ this._sentimentService = new SentimentService(comprehendClient);
+ }
+
+ ///
+ /// A function that takes in the source text and returns the sentiment details.
+ ///
+ /// The extracted text output to analyze.
+ /// The Lambda context
+ /// Sentiment details.
+ public async Task FunctionHandler(SourceTextDetails extractTextOutput, ILambdaContext context)
+ {
+ // Log the object with Lambda PowerTools logger.
+ Logger.LogInformation(extractTextOutput);
+ var result = await _sentimentService.AnalyzeTextSentiment(extractTextOutput.source_text);
+ Logger.LogInformation(result);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj
new file mode 100644
index 00000000000..49d10eefa6e
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/FsaAnalyzeSentiment.csproj
@@ -0,0 +1,21 @@
+
+
+ net6.0
+ enable
+ enable
+ true
+ Lambda
+
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json
new file mode 100644
index 00000000000..a82d4042b00
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Mock Lambda Test Tool": {
+ "commandName": "Executable",
+ "commandLineArgs": "--port 5050",
+ "workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
+ "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json
new file mode 100644
index 00000000000..73bd2a14f0e
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaAnalyzeSentiment/aws-lambda-tools-defaults.json
@@ -0,0 +1,15 @@
+{
+ "Information": [
+ "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
+ "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
+ "dotnet lambda help",
+ "All the command line options for the Lambda command can be specified in this file."
+ ],
+ "profile": "default",
+ "region": "us-east-1",
+ "configuration": "Release",
+ "function-runtime": "dotnet6",
+ "function-memory-size": 256,
+ "function-timeout": 30,
+ "function-handler": "FsaAnalyzeSentiment::FsaAnalyzeSentiment.AnalyzeSentimentFunction::FunctionHandler"
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs
new file mode 100644
index 00000000000..611d38cc001
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/ExtractTextFunction.cs
@@ -0,0 +1,61 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.Json;
+using Amazon.Lambda.Core;
+using Amazon.Textract;
+using AWS.Lambda.Powertools.Logging;
+using FsaServices.Models;
+using FsaServices.Services;
+
+// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
+[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
+
+namespace FsaExtractText;
+
+///
+/// Function to handle extracting the text from an image.
+///
+public class ExtractTextFunction
+{
+ private readonly ExtractionService _extractionService;
+
+ ///
+ /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
+ /// the AWS credentials will come from the IAM role associated with the function and the AWS Region will be set to the
+ /// Region the Lambda function is executed in.
+ ///
+ public ExtractTextFunction()
+ {
+ var textractClient = new AmazonTextractClient();
+ _extractionService = new ExtractionService(textractClient);
+ }
+
+ ///
+ /// Constructs an instance with an Amazon Textract client. This can be used for testing outside of the Lambda environment.
+ ///
+ /// Preconfigured Textract client.
+ public ExtractTextFunction(IAmazonTextract textractClient)
+ {
+ _extractionService = new ExtractionService(textractClient);
+ }
+
+ ///
+ /// This method is called for every Lambda invocation. This method takes in an S3 event object and can be used
+ /// to respond to S3 notifications.
+ ///
+ /// The CloudWatch S3 Event.
+ /// The Lambda context.
+ /// The extracted words as a single string.
+ public async Task FunctionHandler(CardObjectCreated evnt, ILambdaContext context)
+ {
+ // Log the event with Lambda PowerTools logger.
+ var s3Event = evnt;
+ Logger.LogInformation(evnt);
+
+ var extractResponse = await _extractionService.ExtractWordsFromBucketObject(s3Event.Bucket, s3Event.Object);
+ Logger.LogInformation($"Extracted text: {extractResponse}.");
+ return extractResponse;
+
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj
new file mode 100644
index 00000000000..0d71581a032
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/FsaExtractText.csproj
@@ -0,0 +1,25 @@
+
+
+ net6.0
+ enable
+ enable
+ true
+ Lambda
+
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json
new file mode 100644
index 00000000000..a82d4042b00
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Mock Lambda Test Tool": {
+ "commandName": "Executable",
+ "commandLineArgs": "--port 5050",
+ "workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
+ "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json
new file mode 100644
index 00000000000..719aa730e98
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaExtractText/aws-lambda-tools-defaults.json
@@ -0,0 +1,15 @@
+{
+ "Information": [
+ "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
+ "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
+ "dotnet lambda help",
+ "All the command line options for the Lambda command can be specified in this file."
+ ],
+ "profile": "default",
+ "region": "us-east-1",
+ "configuration": "Release",
+ "function-runtime": "dotnet6",
+ "function-memory-size": 256,
+ "function-timeout": 30,
+ "function-handler": "FsaExtractText::FsaExtractText.ExtractTextFunction::FunctionHandler"
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj
new file mode 100644
index 00000000000..01aa8746a36
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/FsaServices.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs
new file mode 100644
index 00000000000..221ea6aad71
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/AudioSourceDestinationDetails.cs
@@ -0,0 +1,25 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace FsaServices.Models;
+
+///
+/// Model class for audio source and destination details.
+///
+public class AudioSourceDestinationDetails
+{
+ ///
+ /// The bucket to use for output.
+ ///
+ public string bucket { get; set; } = null!;
+
+ ///
+ /// The translated text to synthesize.
+ ///
+ public string translated_text { get; set; } = null!;
+
+ ///
+ /// The output object key.
+ ///
+ public string Object { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs
new file mode 100644
index 00000000000..f427ed6ec98
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/CardObjectCreated.cs
@@ -0,0 +1,25 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace FsaServices.Models;
+
+///
+/// Model for card object created event.
+///
+public class CardObjectCreated
+{
+ ///
+ /// The region for the card object.
+ ///
+ public string Region { get; set; } = null!;
+
+ ///
+ /// The bucket for the card object.
+ ///
+ public string Bucket { get; set; } = null!;
+
+ ///
+ /// The object key for the card.
+ ///
+ public string Object { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs
new file mode 100644
index 00000000000..988475cac9d
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SentimentDetails.cs
@@ -0,0 +1,22 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Comprehend;
+
+namespace FsaServices.Models;
+
+///
+/// The details of the analyzed sentiment.
+///
+public class SentimentDetails
+{
+ ///
+ /// The sentiment type of the analyzed text.
+ ///
+ public string sentiment { get; set; } = null!;
+
+ ///
+ /// The language code of the analyzed text.
+ ///
+ public string language_code { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs
new file mode 100644
index 00000000000..59f62507c57
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/SourceTextDetails.cs
@@ -0,0 +1,15 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace FsaServices.Models;
+
+///
+/// Model containing source text details.
+///
+public class SourceTextDetails
+{
+ ///
+ /// The source text.
+ ///
+ public string source_text { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs
new file mode 100644
index 00000000000..471dfa2fb19
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TextWithSourceLanguage.cs
@@ -0,0 +1,20 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace FsaServices.Models;
+
+///
+/// The text and a source language code.
+///
+public class TextWithSourceLanguage
+{
+ ///
+ /// The extracted text.
+ ///
+ public string extracted_text { get; set; } = null!;
+
+ ///
+ /// The source language code.
+ ///
+ public string source_language_code { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs
new file mode 100644
index 00000000000..0c4c0edde5b
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Models/TranslatedTextDetails.cs
@@ -0,0 +1,15 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace FsaServices.Models;
+
+///
+/// Model containing translated text details.
+///
+public class TranslatedTextDetails
+{
+ ///
+ /// The translated text.
+ ///
+ public string translated_text { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs
new file mode 100644
index 00000000000..089d63cfd7a
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/ExtractionService.cs
@@ -0,0 +1,48 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Textract;
+using Amazon.Textract.Model;
+
+namespace FsaServices.Services;
+
+///
+/// Service to handle extracting text from images.
+///
+public class ExtractionService
+{
+ private readonly IAmazonTextract _amazonTextract;
+
+ ///
+ /// Constructor that uses the injected Amazon Textract client.
+ ///
+ /// Amazon Textract client.
+ public ExtractionService(IAmazonTextract amazonTextract)
+ {
+ _amazonTextract = amazonTextract;
+ }
+
+ ///
+ /// Extract the words from a given bucket object and return them in a single string.
+ ///
+ /// The source bucket.
+ /// The key of the bucket object.
+ /// Words as a single string.
+ public async Task ExtractWordsFromBucketObject(string bucket, string name)
+ {
+ var detectTextResponse = await _amazonTextract.DetectDocumentTextAsync(
+ new DetectDocumentTextRequest()
+ {
+ Document = new Document()
+ {
+ S3Object = new S3Object() { Bucket = bucket, Name = name }
+ }
+ });
+
+ var words = detectTextResponse.Blocks
+ .Where(b => b.BlockType == BlockType.WORD)
+ .Select(w => w.Text);
+
+ return string.Join(' ', words);
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs
new file mode 100644
index 00000000000..9d2737bd74f
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SentimentService.cs
@@ -0,0 +1,59 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Comprehend;
+using Amazon.Comprehend.Model;
+using FsaServices.Models;
+
+namespace FsaServices.Services;
+
+///
+/// Service to handle analyzing sentiment from text.
+///
+public class SentimentService
+{
+ private readonly IAmazonComprehend _amazonComprehend;
+
+ ///
+ /// Constructor that uses the injected Amazon Comprehend client.
+ ///
+ /// Amazon Comprehend client.
+ public SentimentService(IAmazonComprehend amazonComprehend)
+ {
+ _amazonComprehend = amazonComprehend;
+ }
+
+ ///
+ /// Analyze and return the sentiment for the source text.
+ ///
+ /// The text to analyze.
+ /// The sentiment details.
+ public async Task AnalyzeTextSentiment(string sourceText)
+ {
+ if (string.IsNullOrEmpty(sourceText))
+ {
+ throw new InvalidOperationException(
+ "Cannot detect sentiment for an empty string.");
+ }
+
+ var languages = await _amazonComprehend.DetectDominantLanguageAsync(
+ new DetectDominantLanguageRequest()
+ {
+ Text = sourceText
+ });
+ var firstLanguage = languages.Languages[0].LanguageCode;
+
+ var sentiment = await _amazonComprehend.DetectSentimentAsync(
+ new DetectSentimentRequest()
+ {
+ LanguageCode = firstLanguage,
+ Text = sourceText
+ });
+
+ return new SentimentDetails()
+ {
+ language_code = firstLanguage,
+ sentiment = sentiment.Sentiment.Value
+ };
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs
new file mode 100644
index 00000000000..3dfd01fdd05
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/SynthesizeService.cs
@@ -0,0 +1,70 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Polly;
+using Amazon.Polly.Model;
+using Amazon.Runtime.Internal;
+using Amazon.S3;
+using Amazon.S3.Model;
+using Amazon.S3.Transfer;
+using FsaServices.Models;
+
+namespace FsaServices.Services;
+
+///
+/// Service to handle synthesizing audio from text.
+///
+public class SynthesizeService
+{
+ private readonly IAmazonPolly _amazonPolly;
+ private readonly IAmazonS3 _amazonS3;
+
+ ///
+ /// Constructor that uses the injected Amazon Polly and S3 clients.
+ ///
+ /// Amazon Polly client.
+ /// Amazon S3 client.
+ public SynthesizeService(IAmazonPolly amazonPolly, IAmazonS3 amazonS3)
+ {
+ _amazonPolly = amazonPolly;
+ _amazonS3 = amazonS3;
+ }
+
+ ///
+ /// Extract the words from a given bucket object and return them in a single string.
+ ///
+ /// The source destination bucket, text, and object ke.y
+ /// The name of the result object in the bucket.
+ public async Task SynthesizeSpeechFromText(AudioSourceDestinationDetails sourceDestinationDetails)
+ {
+ if (string.IsNullOrEmpty(sourceDestinationDetails.translated_text))
+ {
+ throw new InvalidOperationException(
+ "Cannot synthesize audio for an empty string.");
+ }
+
+ var synthesizedResponse = await _amazonPolly.SynthesizeSpeechAsync(
+ new SynthesizeSpeechRequest()
+ {
+ Engine = Engine.Neural,
+ Text = sourceDestinationDetails.translated_text,
+ VoiceId = VoiceId.Ruth,
+ OutputFormat = OutputFormat.Mp3
+ });
+
+ var audioKey = $"{sourceDestinationDetails.Object}.mp3";
+
+ var transfer = new TransferUtility(_amazonS3);
+ var uploadStreamRequest = new TransferUtilityUploadRequest
+ {
+ BucketName = sourceDestinationDetails.bucket,
+ Key = audioKey,
+ InputStream = synthesizedResponse.AudioStream,
+ ContentType = "audio/mpeg",
+ };
+
+ await transfer.UploadAsync(uploadStreamRequest);
+
+ return audioKey;
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs
new file mode 100644
index 00000000000..e5b1338a305
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServices/Services/TranslationService.cs
@@ -0,0 +1,50 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Translate;
+using Amazon.Translate.Model;
+
+namespace FsaServices.Services;
+
+///
+/// Service to handle translating text to English.
+///
+public class TranslationService
+{
+ private readonly IAmazonTranslate _amazonTranslate;
+
+ ///
+ /// Constructor that uses the injected Amazon Translate client.
+ ///
+ /// Amazon Translate client.
+ public TranslationService(IAmazonTranslate amazonTranslate)
+ {
+ _amazonTranslate = amazonTranslate;
+ }
+
+ ///
+ /// Translate a string to the specified target language.
+ ///
+ /// The source text to translate.
+ /// The language code of the source text.
+ /// The translated string.
+ public async Task TranslateToEnglish(string sourceText, string languageCode)
+ {
+ if (string.IsNullOrEmpty(sourceText))
+ {
+ throw new InvalidOperationException(
+ "Cannot translate an empty string.");
+ }
+
+ var translateResponse = await _amazonTranslate.TranslateTextAsync(
+ new TranslateTextRequest()
+ {
+ Text = sourceText,
+ SourceLanguageCode = languageCode,
+ TargetLanguageCode = "en"
+ });
+
+ return translateResponse.TranslatedText;
+ }
+
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs
new file mode 100644
index 00000000000..d01b3c10f11
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/ExtractionServiceTests.cs
@@ -0,0 +1,79 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Textract;
+using Amazon.Textract.Model;
+using FsaServices.Services;
+using Moq;
+
+namespace FsaServicesTest;
+
+///
+/// Tests for the FSA ExtractionService.
+///
+public class ExtractionServiceTests
+{
+ ///
+ /// Verify that extracting words from an object returns a string.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task SynthesizeSpeech_ShouldReturnObjectKey()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var detectDocumentTextResponse = new DetectDocumentTextResponse()
+ {
+ Blocks = new List()
+ {
+ new(){BlockType = BlockType.WORD, Text = "This"},
+ new(){BlockType = BlockType.WORD, Text = "is"},
+ new(){BlockType = BlockType.WORD, Text = "the"},
+ new(){BlockType = BlockType.WORD, Text = "text"},
+ new(){BlockType = BlockType.LINE}
+ }
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectDocumentTextAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(detectDocumentTextResponse);
+
+ var service = new ExtractionService(mockService.Object);
+
+ // Act.
+ var words = await service.ExtractWordsFromBucketObject("testBucket", "testName");
+
+ // Assert.
+ Assert.Equal("This is the text", words);
+ }
+
+ ///
+ /// Verify that no words in the extraction returns an empty string.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task ExtractNoWords_ShouldReturnEmptyString()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var detectDocumentTextResponse = new DetectDocumentTextResponse()
+ {
+ Blocks = new List()
+ {
+ new(){BlockType = BlockType.LINE}
+ }
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectDocumentTextAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(detectDocumentTextResponse);
+
+ var service = new ExtractionService(mockService.Object);
+
+ // Act.
+ var words = await service.ExtractWordsFromBucketObject("testBucket", "testName");
+
+ // Assert.
+ Assert.Equal("", words);
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj
new file mode 100644
index 00000000000..55024b58cfa
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/FsaServicesTest.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs
new file mode 100644
index 00000000000..faabcb241b6
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SentimentServiceTests.cs
@@ -0,0 +1,88 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Comprehend;
+using Amazon.Comprehend.Model;
+using FsaServices.Services;
+using Moq;
+
+namespace FsaServicesTest;
+
+///
+/// Tests for the FSA SentimentService.
+///
+public class SentimentServiceTests
+{
+ ///
+ /// Verify that translating to English returns a string.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task DetectSentiment_ShouldReturnSentimentDetails()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var dominantLanguageResponse = new DetectDominantLanguageResponse()
+ {
+ Languages = new List() { new() { LanguageCode = "en" } }
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectDominantLanguageAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(dominantLanguageResponse);
+
+ var detectSentimentResponse = new DetectSentimentResponse()
+ {
+ Sentiment = SentimentType.POSITIVE
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectSentimentAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(detectSentimentResponse);
+
+ var service = new SentimentService(mockService.Object);
+
+ // Act.
+ var sentimentDetails = await service.AnalyzeTextSentiment("Text to analyze.");
+
+ // Assert.
+ Assert.Equal("en", sentimentDetails.language_code);
+ Assert.Equal(SentimentType.POSITIVE.Value, sentimentDetails.sentiment);
+ }
+
+ ///
+ /// Verify that an empty string throws an Invalid Operation Exception.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task AnalyzeEmptyString_ShouldThrowException()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var dominantLanguageResponse = new DetectDominantLanguageResponse()
+ {
+ Languages = new List() { new() { LanguageCode = "en" } }
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectDominantLanguageAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(dominantLanguageResponse);
+
+ var detectSentimentResponse = new DetectSentimentResponse()
+ {
+ Sentiment = SentimentType.POSITIVE
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.DetectSentimentAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(detectSentimentResponse);
+
+ var service = new SentimentService(mockService.Object);
+
+ // Act and Assert.
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.AnalyzeTextSentiment("");
+ });
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs
new file mode 100644
index 00000000000..d96f3ae9867
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/SynthesizeServiceTests.cs
@@ -0,0 +1,85 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Polly;
+using Amazon.Polly.Model;
+using Amazon.S3;
+using FsaServices.Models;
+using FsaServices.Services;
+using Moq;
+
+namespace FsaServicesTest;
+
+///
+/// Tests for the FSA SynthesizeService.
+///
+public class SynthesizeServiceTests
+{
+ ///
+ /// Verify that translating to English returns a string.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task SynthesizeSpeech_ShouldReturnObjectKey()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var synthesizeSpeechResponse = new SynthesizeSpeechResponse()
+ {
+ AudioStream = new MemoryStream(),
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.SynthesizeSpeechAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(synthesizeSpeechResponse);
+
+ var mockS3Service = new Mock();
+
+ var service = new SynthesizeService(mockService.Object, mockS3Service.Object);
+
+ // Act.
+ var audioKey = await service.SynthesizeSpeechFromText(new AudioSourceDestinationDetails()
+ {
+ bucket = "BucketName",
+ Object = "ObjectKey.jpg",
+ translated_text = "Text to synthesize."
+ });
+
+ // Assert.
+ Assert.Equal("ObjectKey.jpg.mp3", audioKey);
+ }
+
+ ///
+ /// Verify that an empty string throws an Invalid Operation Exception.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task AnalyzeEmptyString_ShouldThrowException()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var synthesizeSpeechResponse = new SynthesizeSpeechResponse()
+ {
+ AudioStream = new MemoryStream(),
+ };
+
+ _ = mockService.Setup(ms =>
+ ms.SynthesizeSpeechAsync(It.IsAny(),
+ CancellationToken.None)).ReturnsAsync(synthesizeSpeechResponse);
+
+ var mockS3Service = new Mock();
+
+ var service = new SynthesizeService(mockService.Object, mockS3Service.Object);
+
+ // Act and Assert.
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.SynthesizeSpeechFromText(new AudioSourceDestinationDetails()
+ {
+ bucket = "BucketName",
+ Object = "ObjectKey.jpg",
+ translated_text = ""
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs
new file mode 100644
index 00000000000..cc3378899a9
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/TranslationServiceTests.cs
@@ -0,0 +1,63 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Translate;
+using Amazon.Translate.Model;
+using FsaServices.Services;
+using Moq;
+
+namespace FsaServicesTest;
+
+///
+/// Tests for the FSA TranslationService.
+///
+public class TranslationServiceTests
+{
+ ///
+ /// Verify that translating to English returns a string.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task TranslateToEnglish_ShouldReturnString()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var responseText = "Text in English.";
+ var response = new TranslateTextResponse { TranslatedText = responseText };
+
+ _ = mockService.Setup(ms =>
+ ms.TranslateTextAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(response);
+
+ var service = new TranslationService(mockService.Object);
+
+ // Act.
+ var translation = await service.TranslateToEnglish("Text in French.", "fr");
+
+ // Assert.
+ Assert.Equal(responseText, translation);
+ }
+
+ ///
+ /// Verify that an empty string throws an Invalid Operation Exception.
+ ///
+ [Fact]
+ [Trait("Category", "Unit")]
+ public async Task TranslateEmptyString_ShouldThrowException()
+ {
+ // Arrange.
+ var mockService = new Mock();
+ var responseText = String.Empty;
+ var response = new TranslateTextResponse { TranslatedText = responseText };
+
+ _ = mockService.Setup(ms =>
+ ms.TranslateTextAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(response);
+
+ var service = new TranslationService(mockService.Object);
+
+ // Act and Assert.
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.TranslateToEnglish("", "fr");
+ });
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs
new file mode 100644
index 00000000000..92fe41799a7
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaServicesTest/Usings.cs
@@ -0,0 +1,4 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+global using Xunit;
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj
new file mode 100644
index 00000000000..3d577fdd16b
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/FsaSynthesizeAudio.csproj
@@ -0,0 +1,21 @@
+
+
+ net6.0
+ enable
+ enable
+ true
+ Lambda
+
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json
new file mode 100644
index 00000000000..a82d4042b00
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Mock Lambda Test Tool": {
+ "commandName": "Executable",
+ "commandLineArgs": "--port 5050",
+ "workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
+ "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs
new file mode 100644
index 00000000000..0256f173458
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/SynthesizeAudioFunction.cs
@@ -0,0 +1,59 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Polly;
+using Amazon.S3;
+using AWS.Lambda.Powertools.Logging;
+using FsaServices.Models;
+using FsaServices.Services;
+
+// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
+[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
+
+namespace FsaSynthesizeAudio;
+
+///
+/// Function to handle synthesizing audio.
+///
+public class SynthesizeAudioFunction
+{
+ private readonly SynthesizeService _synthesizeService;
+
+ ///
+ /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment
+ /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the
+ /// region the Lambda function is executed in.
+ ///
+ public SynthesizeAudioFunction()
+ {
+ var pollyClient = new AmazonPollyClient();
+ var s3Client = new AmazonS3Client();
+ _synthesizeService = new SynthesizeService(pollyClient, s3Client);
+ }
+
+ ///
+ /// Constructs an instance with an Amazon Textract client. This can be used for testing outside of the Lambda environment.
+ ///
+ ///
+ public SynthesizeAudioFunction(IAmazonPolly pollyClient, IAmazonS3 s3Client)
+ {
+ _synthesizeService = new SynthesizeService(pollyClient, s3Client);
+ }
+
+
+ ///
+ /// Takes in the audio source and destination details, and returns the key of the new media object after synthesis.
+ ///
+ /// The source and destination details input.
+ /// The Lambda context.
+ /// The key of the new media object.
+ public async Task FunctionHandler(AudioSourceDestinationDetails input, ILambdaContext context)
+ {
+ // Log the input with Lambda PowerTools logger.
+ Logger.LogInformation(input);
+ var synthesizeResponse = await _synthesizeService.SynthesizeSpeechFromText(input);
+ Logger.LogInformation(synthesizeResponse);
+ return synthesizeResponse;
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json
new file mode 100644
index 00000000000..bc0309cab5b
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaSynthesizeAudio/aws-lambda-tools-defaults.json
@@ -0,0 +1,15 @@
+{
+ "Information": [
+ "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
+ "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
+ "dotnet lambda help",
+ "All the command line options for the Lambda command can be specified in this file."
+ ],
+ "profile": "default",
+ "region": "us-east-1",
+ "configuration": "Release",
+ "function-runtime": "dotnet6",
+ "function-memory-size": 256,
+ "function-timeout": 30,
+ "function-handler": "FsaSynthesizeAudio::FsaSynthesizeAudio.SynthesizeAudioFunction::FunctionHandler"
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj
new file mode 100644
index 00000000000..3d577fdd16b
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/FsaTranslateText.csproj
@@ -0,0 +1,21 @@
+
+
+ net6.0
+ enable
+ enable
+ true
+ Lambda
+
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json
new file mode 100644
index 00000000000..a82d4042b00
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Mock Lambda Test Tool": {
+ "commandName": "Executable",
+ "commandLineArgs": "--port 5050",
+ "workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
+ "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs
new file mode 100644
index 00000000000..0221e7cf4d9
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/TranslateTextFunction.cs
@@ -0,0 +1,56 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.Lambda.Core;
+using Amazon.Translate;
+using AWS.Lambda.Powertools.Logging;
+using FsaServices.Models;
+using FsaServices.Services;
+
+// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
+[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
+
+namespace FsaTranslateText;
+
+///
+/// Function to handle translating the extracted text to English.
+///
+public class TranslateTextFunction
+{
+ private readonly TranslationService _translationService;
+
+ ///
+ /// Default constructor. This constructor is used by AWS Lambda to construct the instance. When invoked in a Lambda environment,
+ /// the AWS credentials will come from the AWS Identity and Access Management (IAM) role associated with the function. The AWS Region will be set to the
+ /// Region the Lambda function is executed in.
+ ///
+ public TranslateTextFunction()
+ {
+ _translationService = new TranslationService(new AmazonTranslateClient());
+ }
+
+ ///
+ /// Constructs an instance with a preconfigured Translate client. This can be used for testing outside of the Lambda environment.
+ ///
+ ///
+ public TranslateTextFunction(IAmazonTranslate translateClient)
+ {
+ this._translationService = new TranslationService(translateClient);
+ }
+
+ ///
+ /// A function that takes in the source text and returns the sentiment details.
+ ///
+ /// The text to analyze.
+ /// The Lambda context
+ /// Sentiment details.
+ public async Task FunctionHandler(TextWithSourceLanguage source_text, ILambdaContext context)
+ {
+ // Log the object with Lambda PowerTools logger.
+ Logger.LogInformation(source_text);
+ var translatedText = await _translationService.TranslateToEnglish(source_text.extracted_text,
+ source_text.source_language_code);
+ Logger.LogInformation(translatedText);
+ return new TranslatedTextDetails() { translated_text = translatedText };
+ }
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json
new file mode 100644
index 00000000000..690dec7aef0
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/FsaTranslateText/aws-lambda-tools-defaults.json
@@ -0,0 +1,15 @@
+{
+ "Information": [
+ "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
+ "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
+ "dotnet lambda help",
+ "All the command line options for the Lambda command can be specified in this file."
+ ],
+ "profile": "default",
+ "region": "us-east-1",
+ "configuration": "Release",
+ "function-runtime": "dotnet6",
+ "function-memory-size": 256,
+ "function-timeout": 30,
+ "function-handler": "FsaTranslateText::FsaTranslateText.TranslateTextFunction::FunctionHandler"
+}
\ No newline at end of file
diff --git a/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md
new file mode 100644
index 00000000000..b83dc81f71b
--- /dev/null
+++ b/dotnetv3/cross-service/FeedbackSentimentAnalyzer/README.md
@@ -0,0 +1,22 @@
+# Create Feedback Sentiment Analyzer (FSA) using the SDK for .NET (v3)
+
+## Overview
+
+This document discusses the language-specific nuances of deploying the Feedback Sentiment Analyzer (FSA) application. For more details, see the application [README.md](../../../applications/feedback_sentiment_analyzer/README.md).
+
+## Prerequisites
+
+- Follow the main [README](../../README.md#Prerequisites) in the `dotnetv3` folder
+- Install the [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)
+
+### Important
+
+- The AWS services included in this document are included in the [AWS Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc).
+- This code has not been tested in all AWS Regions. Some AWS services are available only in specific Regions. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
+- Running this code might result in charges to your AWS account.
+- Be sure to delete all the resources you create while going through this tutorial so that you won't be charged.
+
+### .NET Implementation Details
+ - This example includes AWS Lambda functions for the various operations of the Feedback Sentiment Analyzer.
+ - Each function was created using the AWS Lambda Template from the [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/).
+ - Each function also uses [Powertools for AWS Lamba (.NET)](https://github.com/aws-powertools/powertools-lambda-dotnet) for enhanced logging.
\ No newline at end of file
diff --git a/dotnetv3/cross-service/README.md b/dotnetv3/cross-service/README.md
index 665cb21d6d9..30e8d8e6de4 100644
--- a/dotnetv3/cross-service/README.md
+++ b/dotnetv3/cross-service/README.md
@@ -15,7 +15,25 @@ using the AWS SDK for .NET.
## Cross-service examples
-### [Serverless photo asset management application](PhotoAssetManager/Readme.md)
+### [Feedback Sentiment Analyzer application](FeedbackSentimentAnalyzer/README.md)
+
+Create a Feedback Sentiment Analyzer (FSA) example app that analyzes and stores customer feedback cards. Specifically,
+it fulfills the need of a fictitious hotel in New York City. The hotel receives feedback
+from guests in various languages in the form of physical comment cards. That feedback
+is uploaded into the app through a web client.
+
+The application uses the following services:
+
+- Amazon Textract
+- Amazon Translate
+- Amazon Comprehend
+- Amazon Simple Storage Service (Amazon S3)
+- Amazon Polly
+- AWS Lambda
+- Amazon Cognito
+- Amazon API Gateway
+
+### [Serverless photo asset management application](PhotoAssetManager/README.md)
Create a Photo Asset Management (PAM) example app that uses Amazon Rekognition to categorize images, which are stored with Amazon S3 Intelligent-Tiering for cost savings. Users can upload new images. Those images are analyzed with label detection and the labels are stored in an Amazon DynamoDB table. Users can later request a bundle of images matching those labels. When images are requested, they are retrieved from Amazon S3, zipped, and the user is sent a link to the zip.