Skip to content

Lambda Hosting

AlexaVoxCraft provides optimized AWS Lambda hosting with custom serialization, ReadyToRun publishing, and comprehensive deployment support for Alexa skills.

🎯 Trivia Skill Examples: All code examples show deploying a trivia game skill to AWS Lambda with DynamoDB integration for storing questions and player scores.

🚀 Features

  • ⚡ Custom Runtime: Optimized provided.al2023 runtime with bootstrap handler
  • 🚀 ReadyToRun Publishing: Pre-compiled assemblies for faster cold starts
  • 📦 Self-Contained Deployment: No external dependencies required
  • ⚙ Custom Serialization: Alexa-specific JSON serialization with polymorphic support
  • 📈 Observability: Built-in OpenTelemetry and structured logging
  • 🌐 ICU Support: Internationalization with bundled ICU libraries

Basic Setup

Function Configuration

// Program.cs
using AlexaVoxCraft.MediatR.Lambda;
using AlexaVoxCraft.Model.Apl;
using AlexaVoxCraft.Model.Response;

APLSupport.Add();

return await LambdaHostExtensions.RunAlexaSkill<TriviaSkillFunction, APLSkillRequest, SkillResponse>(
    handlerBuilder: (function, sp) =>
    {
        var tracer = sp.GetRequiredService<TracerProvider>();
        return (req, ctx) => AWSLambdaWrapper.TraceAsync(tracer, function.FunctionHandlerAsync, req, ctx);
    }
);

Skill Function Implementation

public class TriviaSkillFunction : AlexaSkillFunction<APLSkillRequest, SkillResponse>
{
    protected override void Init(IHostBuilder builder)
    {
        builder
            .UseHandler<LambdaHandler, APLSkillRequest, SkillResponse>()
            .ConfigureServices((context, services) =>
            {
                // AWS Configuration
                var options = context.Configuration.GetAWSOptions();
                services.AddDefaultAWSOptions(options);
                services.AddAWSService<IAmazonDynamoDB>();

                // MediatR and handlers
                services.AddSkillMediator(context.Configuration,
                    cfg => cfg.RegisterServicesFromAssemblyContaining<TriviaSkillFunction>());

                // Business services
                services.AddScoped<IGameRepository, GameRepository>();
                services.AddScoped<IGameService, GameService>();
                services.AddScoped<IVisualBuilder, VisualBuilder>();

                // Configuration
                services.Configure<DynamoDbOptions>(opt =>
                    context.Configuration.GetSection(DynamoDbOptions.DynamoDbSettings).Bind(opt));

                // Observability
                services.AddSingleton(_ => Sdk.CreateTracerProviderBuilder()
                    .SetResourceBuilder(ResourceBuilder.CreateDefault()
                        .AddService("TriviaSkill")
                        .AddTelemetrySdk())
                    .AddAWSInstrumentation()
                    .AddSource("TriviaSkill")
                    .AddOtlpExporter()
                    .AddAWSLambdaConfigurations()
                    .Build());

                // Request decorators
                services.Decorate<IRequestHandler<IntentRequest>, ActivitySourceRequestHandlerDecorator<IntentRequest>>();
                services.Decorate<IRequestHandler<LaunchRequest>, ActivitySourceRequestHandlerDecorator<LaunchRequest>>();
            });
    }
}

Project Configuration

Project File Setup

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AWSProjectType>Lambda</AWSProjectType>
    <AssemblyName>bootstrap</AssemblyName>

    <!-- Performance optimizations -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <PublishReadyToRun>true</PublishReadyToRun>
    <LangVersion>default</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <!-- Core AlexaVoxCraft packages -->
    <PackageReference Include="AlexaVoxCraft.MediatR.Lambda" Version="2.0.0.61" />
    <PackageReference Include="AlexaVoxCraft.Model.Apl" Version="2.0.0.61" />

    <!-- AWS Lambda runtime -->
    <PackageReference Include="Amazon.Lambda.Core" Version="2.6.0" />
    <PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.13.1" />

    <!-- AWS services -->
    <PackageReference Include="AWSSDK.DynamoDBv2" Version="4.0.2" />
    <PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="4.0.2" />

    <!-- Observability -->
    <PackageReference Include="OpenTelemetry" Version="1.12.0" />
    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
    <PackageReference Include="OpenTelemetry.Instrumentation.AWS" Version="1.12.0" />
    <PackageReference Include="OpenTelemetry.Instrumentation.AWSLambda" Version="1.12.0" />

    <!-- Additional dependencies -->
    <PackageReference Include="Scrutor" Version="6.1.0" />
    <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
  </ItemGroup>

  <!-- ICU Globalization Support -->
  <ItemGroup>
    <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="72.1.0.3" />
    <PackageReference Include="Microsoft.ICU.ICU4C.Runtime" Version="72.1.0.3" />
  </ItemGroup>
</Project>

AWS Lambda Tools Configuration

// aws-lambda-tools-defaults.json
{
  "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": "",
  "region": "",
  "configuration": "Release",
  "function-runtime": "provided.al2023",
  "function-memory-size": 512,
  "function-timeout": 30,
  "function-handler": "bootstrap",
  "msbuild-parameters": "--self-contained true"
}

Lambda Handler Implementation

Core Handler

public class LambdaHandler : ILambdaHandler<APLSkillRequest, SkillResponse>
{
    private readonly ISkillMediator _mediator;
    private readonly ILogger<LambdaHandler> _logger;

    public LambdaHandler(ISkillMediator mediator, ILogger<LambdaHandler> logger)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<SkillResponse> HandleAsync(APLSkillRequest request, ILambdaContext context)
    { 
        using var activity = DiagnosticsConfig.Source.StartActivityWithTags(
            $"{nameof(LambdaHandler)}.{nameof(HandleAsync)}", new()
            {
                new("rpc.service", nameof(LambdaHandler)),
                new("rpc.system", "AlexaVoxCraft"),
                new("request.type", request.Request.GetType().Name)
            });

        if (request.Request is IntentRequest intent)
        {
            _logger.LogDebug("Received intent {intentType}", intent.Intent.Name);
            activity?.SetTag("alexa.intent.name", intent.Intent.Name);
        }

        _logger.LogDebug("Received request of type {requestType}", request.Request.Type);

        try
        {
            var response = await _mediator.Send(request);
            activity?.SetStatus(ActivityStatusCode.Ok);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error handling request");
            activity?.AddException(ex);
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

Activity Source Decorator

public class ActivitySourceRequestHandlerDecorator<TRequest> : IRequestHandler<TRequest>
    where TRequest : Request
{
    private readonly IRequestHandler<TRequest> _decorated;
    private readonly ActivitySource _activitySource;

    public ActivitySourceRequestHandlerDecorator(IRequestHandler<TRequest> decorated)
    {
        _decorated = decorated;
        _activitySource = new ActivitySource("TriviaSkill");
    }

    public Task<bool> CanHandle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
    {
        return _decorated.CanHandle(handlerInput, cancellationToken);
    }

    public async Task<SkillResponse> Handle(IHandlerInput handlerInput, CancellationToken cancellationToken = default)
    {
        using var activity = _activitySource.StartActivity($"{_decorated.GetType().Name}.Handle");

        activity?.SetTag("alexa.request.type", typeof(TRequest).Name);
        activity?.SetTag("alexa.user.id", handlerInput.RequestEnvelope.GetUserId());

        try
        {
            var result = await _decorated.Handle(handlerInput, cancellationToken);
            activity?.SetStatus(ActivityStatusCode.Ok);
            return result;
        }
        catch (Exception ex)
        {
            activity?.AddException(ex);
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

Configuration

Application Settings

// appsettings.json
{
  "AWS": {
    "Region": "us-east-1"
  },
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console"
    ],
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "formatter": "LayeredCraft.Logging.CompactJsonFormatter.CompactJsonFormatter, LayeredCraft.Logging.CompactJsonFormatter"
        }
      }
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning"
      }
    }
  },
  "DynamoDbSettings": {
    "TableMaps": {
      "GameRepository": {
        "TableName": "trivia-skill-table",
        "IndexName": "gs1-index"
      }
    }
  }
}

Environment-Specific Configuration

// Configuration helper
public static class ConfigurationExtensions
{
    public static void ConfigureForEnvironment(this IServiceCollection services, 
        IConfiguration configuration, string environment)
    {
        switch (environment.ToLower())
        {
            case "development":
                services.Configure<DynamoDbOptions>(opt =>
                {
                    opt.TableMaps["GameRepository"].TableName = "trivia-skill-dev";
                });
                break;

            case "production":
                services.Configure<DynamoDbOptions>(opt =>
                {
                    opt.TableMaps["GameRepository"].TableName = "trivia-skill-prod";
                });
                break;
        }
    }
}

Deployment

Building and Packaging

# Install AWS Lambda Tools (one-time setup)
dotnet new tool-manifest --force
dotnet tool install Amazon.Lambda.Tools
dotnet restore

# Create deployment package
dotnet lambda package

# Deploy to AWS
dotnet lambda deploy-function MySkillFunction \
  --function-role arn:aws:iam::123456789012:role/lambda-execution-role \
  --region us-east-1

Package Optimization

# Self-contained with ReadyToRun
dotnet publish -c Release \
  --self-contained true \
  --runtime linux-x64 \
  -p:PublishReadyToRun=true \
  -p:PublishSingleFile=false

Docker Container Support

# Dockerfile for container deployment
FROM public.ecr.aws/lambda/dotnet:9-x86_64

# Copy built application
COPY publish/ ${LAMBDA_TASK_ROOT}/

# Set the CMD to your handler
CMD ["bootstrap"]

Performance Optimization

ReadyToRun Benefits

ReadyToRun compilation provides: - Faster application startup - Reduced cold start times - Better performance for CPU-intensive operations - Native code generation for frequently used paths

Memory and Timeout Configuration

// Recommended Lambda configuration
Memory: 512 MB - 1024 MB (depending on complexity)
Timeout: 30 seconds (Alexa maximum)
Runtime: provided.al2023

Bundle Size Optimization

<!-- Enable trimming for smaller packages -->
<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>copyused</TrimMode>
</PropertyGroup>

<!-- Preserve necessary assemblies -->
<ItemGroup>
  <TrimmerRootAssembly Include="AlexaVoxCraft.Model" />
  <TrimmerRootAssembly Include="AlexaVoxCraft.Model.Apl" />
</ItemGroup>

Monitoring and Logging

CloudWatch Integration

// CloudWatch-compatible JSON logging
"Serilog": {
  "WriteTo": [
    {
      "Name": "Console",
      "Args": {
        "formatter": "LayeredCraft.Logging.CompactJsonFormatter.CompactJsonFormatter, LayeredCraft.Logging.CompactJsonFormatter"
      }
    }
  ]
}

Custom Metrics

public class DiagnosticsConfig
{
    public static readonly ActivitySource Source = new("TriviaSkill");
    public static readonly string ServiceName = "TriviaSkill";
    public static readonly string SystemName = "AlexaVoxCraft";

    public static readonly Counter<int> CorrectCounter = 
        Metrics.CreateCounter<int>("trivia_correct_answers", "Number of correct answers");

    public static readonly Counter<int> IncorrectCounter = 
        Metrics.CreateCounter<int>("trivia_incorrect_answers", "Number of incorrect answers");
}

// Usage in handlers
DiagnosticsConfig.CorrectCounter.Add(1, new KeyValuePair<string, object>[]
{
    new("user.id", userId),
    new("question.category", "general")
});

Request/Response Logging

// Enable detailed request/response logging in development
"AlexaVoxCraft.MediatR.Lambda.Serialization": "Debug"

Security

IAM Permissions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query",
        "dynamodb:UpdateItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/trivia-skill-*"
    }
  ]
}

Environment Variables

// Secure configuration access
var connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
var apiKey = Environment.GetEnvironmentVariable("EXTERNAL_API_KEY");

Testing Locally

AWS Lambda Test Tool

# Install test tool
dotnet tool install -g Amazon.Lambda.TestTool-9.0

# Run locally
dotnet lambda-test-tool-9.0

Mock Alexa Requests

// Test with mock requests
var request = new APLSkillRequest
{
    Version = "1.0",
    Session = new Session
    {
        New = true,
        SessionId = "test-session"
    },
    Request = new LaunchRequest
    {
        RequestId = "test-request",
        Timestamp = DateTimeOffset.UtcNow
    }
};

var response = await handler.HandleAsync(request, mockContext);

Best Practices

1. Use Dependency Injection

// Register services properly
services.AddScoped<IMyService, MyService>();
services.AddSingleton<IConfiguration>(configuration);

2. Handle Cold Starts

// Initialize expensive resources outside handler
private static readonly HttpClient HttpClient = new();
private static readonly TracerProvider TracerProvider = CreateTracer();

3. Implement Proper Cancellation

public async Task<SkillResponse> Handle(IHandlerInput input, CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    // Use cancellation token in async operations
}

4. Log Structured Data

_logger.LogInformation("Processing {RequestType} for user {UserId}", 
    request.GetType().Name, 
    userId);

Examples

For complete deployment examples, see the Examples section with CDK infrastructure and CI/CD pipelines.