Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Serverless/Building an Alexa Skill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Building an Alexa Skill with AWS Lambda

Alexa Skill developers have the ability to create skills that execute an AWS Lambda Function on the backend to complete a task needed for the skill. Since AWS Lamdba functions can be written in .NET, it is exciting to see what great things we can build for smart home assistants like Amazon Echo.

This code repository accompanies a series of blog posts titled [Building an Alexa Skill with AWS Lambda and Amazon DynamoDB](ADD LINK LATER)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Amazon.Lambda.Core;
using Microsoft.Extensions.DependencyInjection;

// 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 TMDBAlexa.SeedData;

public class Function
{
private readonly LambdaEntryPoint _entryPoint;

public Function()
{
var startup = new Startup();
IServiceProvider provider = startup.Setup();

_entryPoint = provider.GetRequiredService<LambdaEntryPoint>();
}

public async Task<string> FunctionHandler(ILambdaContext context)
{
return await _entryPoint.Handler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Amazon.DynamoDBv2.Model;
using Amazon.DynamoDBv2;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMDbLib.Client;
using Microsoft.Extensions.Configuration;
using TMDBAlexa.Shared;

namespace TMDBAlexa.SeedData
{
public class LambdaEntryPoint
{
private readonly ILogger<LambdaEntryPoint> _logger;
private readonly DynamoDBService _dbService;
private readonly IConfiguration _config;
public LambdaEntryPoint(ILogger<LambdaEntryPoint> logger, DynamoDBService dbService, IConfiguration config)
{
_logger = logger;
_dbService = dbService;
_config = config;
}

public async Task<string> Handler()
{
_logger.LogInformation("Handler invoked");

TMDbClient client = new TMDbClient(_config["TMDBApiKey"]);

await _dbService.CreateTable();

for (int i = 1; i < 100; i++)
{
var results = await client.GetMoviePopularListAsync("en-US", i);

await _dbService.WriteToTable(results);
}

return "Done";
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Amazon.DynamoDBv2;
using Amazon.Extensions.NETCore.Setup;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMDBAlexa.Shared;

namespace TMDBAlexa.SeedData
{
public class Startup
{
public IServiceProvider Setup()
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();

var services = new ServiceCollection();

services.AddSingleton<IConfiguration>(configuration);

services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
});


ConfigureServices(configuration, services);

IServiceProvider provider = services.BuildServiceProvider();

return provider;
}

private void ConfigureServices(IConfiguration configuration, ServiceCollection services)
{
AWSOptions awsOptions = configuration.GetAWSOptions();
services.AddDefaultAWSOptions(awsOptions);
services.AddAWSService<IAmazonDynamoDB>();

services.AddSingleton<LambdaEntryPoint, LambdaEntryPoint>();
services.AddSingleton<DynamoDBService, DynamoDBService>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<AWSProjectType>Lambda</AWSProjectType>
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Generate ready to run images during publishing to improve cold start time. -->
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.0" />
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.3.66" />
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />

<PackageReference Include="TMDbLib" Version="1.9.2" />

<!-- Packages required for Configuration -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<!---->

<!-- Packages required for Logging -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TMDBAlexa.Shared\TMDBAlexa.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"TMDBApiKey": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

{
"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-west-2",
"configuration" : "Release",
"function-runtime" : "dotnet6",
"function-memory-size" : 256,
"function-timeout" : 30,
"function-handler" : "TMDBAlexa.SeedData::TMDBAlexa.SeedData.Function::FunctionHandler",
"framework" : "net6.0",
"function-name" : "TMDBAlexaSeedData",
"package-type" : "Zip",
"function-role" : "arn:aws:iam::563011967245:role/lambda_exec_TMDBAlexaSeedData",
"function-architecture" : "x86_64",
"function-subnets" : "",
"function-security-groups" : "",
"tracing-mode" : "PassThrough",
"environment-variables" : "",
"image-tag" : "",
"function-description" : ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.Model;
using Microsoft.Extensions.Logging;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;

namespace TMDBAlexa.Shared
{
public class DynamoDBService
{
private readonly ILogger<DynamoDBService> _logger;
private readonly AmazonDynamoDBClient _client = new AmazonDynamoDBClient();
private readonly string _tableName = "movies";

public DynamoDBService(ILogger<DynamoDBService> logger)
{
_logger = logger;
}

public async Task<MovieModel> SearchTable(string searchText)
{
DynamoDBContext context = new DynamoDBContext(_client);

var conditions = new List<ScanCondition>()
{
new ScanCondition("TitleLower", Amazon.DynamoDBv2.DocumentModel.ScanOperator.Contains, searchText.ToLower())
};

var queryResult = await context.ScanAsync<MovieModel>(conditions).GetRemainingAsync();

if (queryResult.Count > 0)
{
return queryResult.FirstOrDefault();
}
return null;
}

public async Task CreateTable()
{
await DeleteTable();
var request = new CreateTableRequest
{
AttributeDefinitions = new List<AttributeDefinition>()
{
new AttributeDefinition
{
AttributeName = "id",
AttributeType = "N"
},
new AttributeDefinition
{
AttributeName = "popularity",
AttributeType = "N"
}
},
KeySchema = new List<KeySchemaElement>
{
new KeySchemaElement
{
AttributeName = "id",
KeyType = "HASH" //Partition key
},
new KeySchemaElement
{
AttributeName = "popularity",
KeyType = "RANGE" //Sort key
}
},
ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 5,
WriteCapacityUnits = 6
},
TableName = _tableName
};

var response = await _client.CreateTableAsync(request);

var tableDescription = response.TableDescription;
_logger.LogInformation("{1}: {0} \t ReadsPerSec: {2} \t WritesPerSec: {3}",
tableDescription.TableStatus,
tableDescription.TableName,
tableDescription.ProvisionedThroughput.ReadCapacityUnits,
tableDescription.ProvisionedThroughput.WriteCapacityUnits);

string status = tableDescription.TableStatus;
_logger.LogInformation(_tableName + " - " + status);

await WaitUntilTableReady(_tableName);
}

public async Task WriteToTable(SearchContainer<SearchMovie> results)
{
DynamoDBContext context = new DynamoDBContext(_client);

BatchWrite<MovieModel> model = context.CreateBatchWrite<MovieModel>();

foreach (var movie in results.Results)
{
model.AddPutItem(new MovieModel
{
Id = movie.Id,
Overview = movie.Overview,
Popularity = movie.Popularity,
ReleaseDate = movie.ReleaseDate.ToString(),
Title = movie.Title,
TitleLower = movie.Title.ToLower(),
VoteAverage = movie.VoteAverage,
VoteCount = movie.VoteCount
});
}

await model.ExecuteAsync();
}

private async Task DeleteTable()
{
try
{
await _client.DescribeTableAsync(new DescribeTableRequest
{
TableName = _tableName
});

DeleteTableRequest deleteRequest = new DeleteTableRequest
{
TableName = _tableName
};

await _client.DeleteTableAsync(deleteRequest);

Thread.Sleep(5000);
}
catch (ResourceNotFoundException)
{ }
}

private async Task WaitUntilTableReady(string tableName)
{
string status = null;
// Let us wait until table is created. Call DescribeTable.
do
{
System.Threading.Thread.Sleep(5000); // Wait 5 seconds.
try
{
var res = await _client.DescribeTableAsync(new DescribeTableRequest
{
TableName = tableName
});

_logger.LogInformation("Table name: {0}, status: {1}",
res.Table.TableName,
res.Table.TableStatus);
status = res.Table.TableStatus;
}
catch (ResourceNotFoundException)
{
// DescribeTable is eventually consistent. So you might
// get resource not found. So we handle the potential exception.
}
} while (status != "ACTIVE");
}
}
}
Loading