The AWS documentation gets you a "Hello World" Lambda in ten minutes. Getting a .NET Lambda function ready for production — testable, observable, and safe to deploy without surprises — takes a bit more structure. Here's what we standardize on for client work.

Key takeaways
  • Use the generic host (Amazon.Lambda.RuntimeSupport) so you can register DI, configuration, and logging exactly like an ASP.NET Core app.
  • Keep the handler thin — it should parse the event, call into your Application layer, and map the result. No business logic in Function.cs.
  • Configure a dead-letter queue or an on-failure destination for every asynchronously invoked function; silent drops are worse than a loud failure.
  • Emit structured logs and enable X-Ray tracing from day one — retrofitting observability after an incident is the hard way to learn this lesson.

Project structure

Treat the Lambda function like any other entry point into your Application layer — not a place where business logic lives.

bash
src/
  OrderProcessing.Domain/
  OrderProcessing.Application/
  OrderProcessing.Infrastructure/
  OrderProcessing.Lambda/
    Function.cs
    Startup.cs
    aws-lambda-tools-defaults.json

Wiring dependency injection with the generic host

Skip the static-class-with-manually-constructed-dependencies pattern. The generic host lets you use the same IServiceCollection registration style as an ASP.NET Core project.

csharp
public class Function
{
    private static readonly IServiceProvider _services = ConfigureServices();

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();
        services.AddLogging(b => b.AddLambdaLogger());
        services.AddSingleton<IAmazonDynamoDB>(new AmazonDynamoDBClient());
        services.AddScoped<IOrderRepository, DynamoOrderRepository>();
        services.AddScoped<SubmitOrderHandler>();
        return services.BuildServiceProvider();
    }

    public async Task<APIGatewayProxyResponse> FunctionHandler(
        APIGatewayProxyRequest request, ILambdaContext context)
    {
        using var scope = _services.CreateScope();
        var handler = scope.ServiceProvider.GetRequiredService<SubmitOrderHandler>();

        var command = JsonSerializer.Deserialize<SubmitOrderCommand>(request.Body)
            ?? throw new BadRequestException("Invalid request body.");

        await handler.Handle(command, context.RemainingTime());

        return new APIGatewayProxyResponse
        {
            StatusCode = 204,
            Headers = new Dictionary<string, string> { ["Content-Type"] = "application/json" }
        };
    }
}

Note the CreateScope() per invocation — the container itself is built once (outside the handler) so it survives warm starts, but scoped services still get a fresh lifetime per request, matching how you'd reason about them in ASP.NET Core.

Error handling: fail loudly, not silently

For functions invoked asynchronously (S3 events, SNS, EventBridge), configure an on-failure destination or a dead-letter queue. Without one, a function that exhausts its retries simply drops the event with no trace.

json
{
  "DestinationConfig": {
    "OnFailure": {
      "Destination": "arn:aws:sqs:us-east-1:123456789012:order-processing-dlq"
    }
  },
  "MaximumRetryAttempts": 2
}
Watch forFor functions triggered by SQS, a misbehaving message can block the whole queue if you don't configure a per-queue dead-letter queue with a maxReceiveCount. Without it, one poison message retries forever and starves every other message behind it.

Structured logging and tracing

Plain Console.WriteLine logs are hard to query once you have more than a handful of invocations a day. Use structured logging so CloudWatch Logs Insights can filter and aggregate by field, and enable AWS X-Ray for end-to-end traces across Lambda, DynamoDB, and downstream HTTP calls.

csharp
_logger.LogInformation(
    "Order {OrderId} submitted for customer {CustomerId} in {ElapsedMs}ms",
    order.Id, order.CustomerId, stopwatch.ElapsedMilliseconds);

Least-privilege IAM, every time

Resist the temptation to attach a broad managed policy while iterating and "tighten it up later." Scope the execution role to the specific table, queue, and secret ARNs the function actually touches from the start — it's much cheaper to write a precise policy up front than to audit a wildcard policy after the fact.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.