diff --git a/Serverless/Building an Alexa Skill/README.md b/Serverless/Building an Alexa Skill/README.md new file mode 100644 index 0000000..fe17f28 --- /dev/null +++ b/Serverless/Building an Alexa Skill/README.md @@ -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) diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Function.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Function.cs new file mode 100644 index 0000000..7b2dffa --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Function.cs @@ -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(); + } + + public async Task FunctionHandler(ILambdaContext context) + { + return await _entryPoint.Handler(); + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/LambdaEntryPoint.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/LambdaEntryPoint.cs new file mode 100644 index 0000000..3544286 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/LambdaEntryPoint.cs @@ -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 _logger; + private readonly DynamoDBService _dbService; + private readonly IConfiguration _config; + public LambdaEntryPoint(ILogger logger, DynamoDBService dbService, IConfiguration config) + { + _logger = logger; + _dbService = dbService; + _config = config; + } + + public async Task 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"; + } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Properties/launchSettings.json b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Properties/launchSettings.json new file mode 100644 index 0000000..a82d404 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/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/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Startup.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Startup.cs new file mode 100644 index 0000000..1259f36 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/Startup.cs @@ -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(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(); + + services.AddSingleton(); + services.AddSingleton(); + } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/TMDBAlexa.SeedData.csproj b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/TMDBAlexa.SeedData.csproj new file mode 100644 index 0000000..36814bd --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/TMDBAlexa.SeedData.csproj @@ -0,0 +1,41 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/appsettings.json b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/appsettings.json new file mode 100644 index 0000000..a32c5b9 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/appsettings.json @@ -0,0 +1,3 @@ +{ + "TMDBApiKey": "" +} \ No newline at end of file diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/aws-lambda-tools-defaults.json b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..b7520a6 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.SeedData/aws-lambda-tools-defaults.json @@ -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" : "" +} \ No newline at end of file diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/DynamoDBService.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/DynamoDBService.cs new file mode 100644 index 0000000..ae9d080 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/DynamoDBService.cs @@ -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 _logger; + private readonly AmazonDynamoDBClient _client = new AmazonDynamoDBClient(); + private readonly string _tableName = "movies"; + + public DynamoDBService(ILogger logger) + { + _logger = logger; + } + + public async Task SearchTable(string searchText) + { + DynamoDBContext context = new DynamoDBContext(_client); + + var conditions = new List() + { + new ScanCondition("TitleLower", Amazon.DynamoDBv2.DocumentModel.ScanOperator.Contains, searchText.ToLower()) + }; + + var queryResult = await context.ScanAsync(conditions).GetRemainingAsync(); + + if (queryResult.Count > 0) + { + return queryResult.FirstOrDefault(); + } + return null; + } + + public async Task CreateTable() + { + await DeleteTable(); + var request = new CreateTableRequest + { + AttributeDefinitions = new List() + { + new AttributeDefinition + { + AttributeName = "id", + AttributeType = "N" + }, + new AttributeDefinition + { + AttributeName = "popularity", + AttributeType = "N" + } + }, + KeySchema = new List + { + 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 results) + { + DynamoDBContext context = new DynamoDBContext(_client); + + BatchWrite model = context.CreateBatchWrite(); + + 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"); + } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/MovieModel.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/MovieModel.cs new file mode 100644 index 0000000..5c7ffce --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/MovieModel.cs @@ -0,0 +1,30 @@ +using Amazon.DynamoDBv2.DataModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TMDBAlexa.Shared +{ + [DynamoDBTable("movies")] + public class MovieModel + { + [DynamoDBHashKey("id")] + public int Id { get; set; } + [DynamoDBProperty("title")] + public string Title { get; set; } + [DynamoDBProperty("title_lower")] + public string TitleLower { get; set; } + [DynamoDBProperty("overview")] + public string Overview { get; set; } + [DynamoDBProperty("popularity")] + public double Popularity { get; set; } + [DynamoDBProperty("voteAverage")] + public double VoteAverage { get; set; } + [DynamoDBProperty("voteCount")] + public int VoteCount { get; set; } + [DynamoDBProperty("releaseDate")] + public string ReleaseDate { get; set; } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/TMDBAlexa.Shared.csproj b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/TMDBAlexa.Shared.csproj new file mode 100644 index 0000000..666eb04 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Shared/TMDBAlexa.Shared.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Function.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Function.cs new file mode 100644 index 0000000..6402039 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Function.cs @@ -0,0 +1,33 @@ +using Alexa.NET; +using Alexa.NET.Request; +using Alexa.NET.Request.Type; +using Alexa.NET.Response; +using Amazon.Lambda.Core; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using TMDbLib.Objects.Movies; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace TMDBAlexa.Skill; + +public class Function +{ + private readonly LambdaEntryPoint _entryPoint; + + public Function() + { + var startup = new Startup(); + IServiceProvider provider = startup.Setup(); + + _entryPoint = provider.GetRequiredService(); + } + + public async Task FunctionHandler(SkillRequest input, ILambdaContext context) + { + ILambdaLogger log = context.Logger; + log.LogLine($"Skill Request Object:" + JsonConvert.SerializeObject(input)); + return await _entryPoint.Handler(input); + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/LambdaEntryPoint.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/LambdaEntryPoint.cs new file mode 100644 index 0000000..f80d9e9 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/LambdaEntryPoint.cs @@ -0,0 +1,70 @@ +using Alexa.NET; +using Alexa.NET.Request; +using Alexa.NET.Request.Type; +using Alexa.NET.Response; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TMDBAlexa.Shared; + +namespace TMDBAlexa.Skill +{ + public class LambdaEntryPoint + { + private readonly ILogger _logger; + private readonly DynamoDBService _dbService; + + public LambdaEntryPoint(ILogger logger, DynamoDBService dbService) + { + _logger = logger; + _dbService = dbService; + } + + public async Task Handler(SkillRequest input) + { + Type requestType = input.GetRequestType(); + if (input.GetRequestType() == typeof(LaunchRequest)) + { + string speech = "Please provide a movie you would like to know more about"; + Reprompt rp = new Reprompt("Provide a movie"); + return ResponseBuilder.Ask(speech, rp); + } + else if (input.GetRequestType() == typeof(SessionEndedRequest)) + { + return ResponseBuilder.Tell("Goodbye!"); + } + else if (input.GetRequestType() == typeof(IntentRequest)) + { + var intentRequest = (IntentRequest)input.Request; + switch (intentRequest.Intent.Name) + { + case "AMAZON.CancelIntent": + case "AMAZON.StopIntent": + return ResponseBuilder.Tell("Thanks for using the TMDB Skill!"); + case "AMAZON.HelpIntent": + { + Reprompt rp = new Reprompt("What's next?"); + return ResponseBuilder.Ask("Here's some help. What's next?", rp); + } + case "GetMovieInfo": + { + var movie = await _dbService.SearchTable(intentRequest.Intent.Slots["Movie"].Value); + + if (movie != null) + { + return ResponseBuilder.Tell($"{movie.Title} was released on {DateTime.Parse(movie.ReleaseDate).ToShortDateString()}"); + } + return ResponseBuilder.Tell($"No movies found, please try again"); + } + default: + { + _logger.LogInformation($"Unknown intent: " + intentRequest.Intent.Name); + string speech = "I didn't understand - try again?"; + Reprompt rp = new Reprompt(speech); + return ResponseBuilder.Ask(speech, rp); + } + } + } + return ResponseBuilder.Tell("Thanks for using the TMDB Skill!"); + } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Properties/launchSettings.json b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Properties/launchSettings.json new file mode 100644 index 0000000..a82d404 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/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/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Startup.cs b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Startup.cs new file mode 100644 index 0000000..3177728 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/Startup.cs @@ -0,0 +1,60 @@ +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.Skill +{ + public class Startup + { + public IServiceProvider Setup() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddEnvironmentVariables() + .Build(); + + var services = new ServiceCollection(); + + // Add configuration service + services.AddSingleton(configuration); + + // Add logging service + services.AddLogging(loggingBuilder => + { + loggingBuilder.ClearProviders(); + }); + + + ConfigureServices(configuration, services); + + IServiceProvider provider = services.BuildServiceProvider(); + + return provider; + } + + private void ConfigureServices(IConfiguration configuration, ServiceCollection services) + { + #region AWS SDK setup + // Get the AWS profile information from configuration providers + AWSOptions awsOptions = configuration.GetAWSOptions(); + + // Configure AWS service clients to use these credentials + services.AddDefaultAWSOptions(awsOptions); + + // These AWS service clients will be singleton by default + services.AddAWSService(); + #endregion + + services.AddSingleton(); + services.AddSingleton(); + } + } +} diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/TMDBAlexa.Skill.csproj b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/TMDBAlexa.Skill.csproj new file mode 100644 index 0000000..9b1ba7a --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/TMDBAlexa.Skill.csproj @@ -0,0 +1,38 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/aws-lambda-tools-defaults.json b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..2ff66ec --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.Skill/aws-lambda-tools-defaults.json @@ -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.Skill::TMDBAlexa.Skill.Function::FunctionHandler", + "framework" : "net6.0", + "function-name" : "TMDBAlexaSkill", + "function-description" : "", + "package-type" : "Zip", + "function-role" : "arn:aws:iam::563011967245:role/lambda_exec_TMDBAlexaSkill", + "function-architecture" : "x86_64", + "function-subnets" : "", + "function-security-groups" : "", + "tracing-mode" : "PassThrough", + "environment-variables" : "", + "image-tag" : "" +} \ No newline at end of file diff --git a/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.sln b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.sln new file mode 100644 index 0000000..97f2d40 --- /dev/null +++ b/Serverless/Building an Alexa Skill/TMDBAlexa/TMDBAlexa.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32728.150 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TMDBAlexa.SeedData", "TMDBAlexa.SeedData\TMDBAlexa.SeedData.csproj", "{0FA6B2A3-F020-4D2B-8375-2580530B49D6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TMDBAlexa.Shared", "TMDBAlexa.Shared\TMDBAlexa.Shared.csproj", "{58EB52D9-7D4E-462D-B5FC-8A0B4B489BA4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TMDBAlexa.Skill", "TMDBAlexa.Skill\TMDBAlexa.Skill.csproj", "{37EE6586-3A93-472B-B2EE-CF3F7859B951}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0FA6B2A3-F020-4D2B-8375-2580530B49D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FA6B2A3-F020-4D2B-8375-2580530B49D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FA6B2A3-F020-4D2B-8375-2580530B49D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FA6B2A3-F020-4D2B-8375-2580530B49D6}.Release|Any CPU.Build.0 = Release|Any CPU + {58EB52D9-7D4E-462D-B5FC-8A0B4B489BA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58EB52D9-7D4E-462D-B5FC-8A0B4B489BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58EB52D9-7D4E-462D-B5FC-8A0B4B489BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58EB52D9-7D4E-462D-B5FC-8A0B4B489BA4}.Release|Any CPU.Build.0 = Release|Any CPU + {37EE6586-3A93-472B-B2EE-CF3F7859B951}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37EE6586-3A93-472B-B2EE-CF3F7859B951}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37EE6586-3A93-472B-B2EE-CF3F7859B951}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37EE6586-3A93-472B-B2EE-CF3F7859B951}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD7EE5EB-6853-4A44-B985-08D0EAF2806C} + EndGlobalSection +EndGlobal