Traditional Lambda Functions in C#
This tutorial covers the foundational approach to building AWS Lambda functions in C#. Understanding this baseline is essential before exploring modern patterns like Lambda Annotations and LambdaOpenApi.
This tutorial demonstrates the traditional approach without LambdaOpenApi or Lambda Annotations. While functional, this method requires significant boilerplate code. After completing this tutorial, we recommend progressing to Lambda Annotations to see how modern tooling reduces complexity.
Prerequisites
- .NET 8 SDK or later
- AWS account with Lambda access
- Basic familiarity with C# and AWS concepts
Project Setup
Create a New Lambda Project
Start by creating a new .NET class library project:
dotnet new classlib -n MyLambdaFunction
cd MyLambdaFunction
Install Required NuGet Packages
Add the essential packages for Lambda development:
dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.APIGatewayEvents
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
| Package | Purpose |
|---|---|
Amazon.Lambda.Core | Core Lambda interfaces and attributes |
Amazon.Lambda.APIGatewayEvents | Request/response types for API Gateway integration |
Amazon.Lambda.Serialization.SystemTextJson | JSON serialization for Lambda payloads |
Code Example
Here's a complete Lambda function that handles an API Gateway request to retrieve a product by ID:
The Product Model
namespace MyLambdaFunction.Models;
public class Product
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
}
The Lambda Function Handler
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using System.Text.Json;
using MyLambdaFunction.Models;
// 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 MyLambdaFunction;
public class Functions
{
/// <summary>
/// Lambda function handler for GET /products/{id}
/// </summary>
public APIGatewayProxyResponse GetProduct(
APIGatewayProxyRequest request,
ILambdaContext context)
{
// Manual extraction of path parameters
if (!request.PathParameters.TryGetValue("id", out var productId))
{
return new APIGatewayProxyResponse
{
StatusCode = 400,
Body = JsonSerializer.Serialize(new { error = "Product ID is required" }),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
}
};
}
context.Logger.LogInformation($"Getting product with ID: {productId}");
// Simulate fetching product (in real app, this would query a database)
var product = new Product
{
Id = productId,
Name = "Sample Product",
Price = 29.99m,
Description = "A sample product for demonstration"
};
// Manual response construction
return new APIGatewayProxyResponse
{
StatusCode = 200,
Body = JsonSerializer.Serialize(product),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
}
};
}
/// <summary>
/// Lambda function handler for POST /products
/// </summary>
public APIGatewayProxyResponse CreateProduct(
APIGatewayProxyRequest request,
ILambdaContext context)
{
// Manual deserialization of request body
if (string.IsNullOrEmpty(request.Body))
{
return new APIGatewayProxyResponse
{
StatusCode = 400,
Body = JsonSerializer.Serialize(new { error = "Request body is required" }),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
}
};
}
Product? product;
try
{
product = JsonSerializer.Deserialize<Product>(request.Body);
}
catch (JsonException)
{
return new APIGatewayProxyResponse
{
StatusCode = 400,
Body = JsonSerializer.Serialize(new { error = "Invalid JSON in request body" }),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
}
};
}
if (product == null)
{
return new APIGatewayProxyResponse
{
StatusCode = 400,
Body = JsonSerializer.Serialize(new { error = "Invalid product data" }),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
}
};
}
// Assign an ID to the new product
product.Id = Guid.NewGuid().ToString();
context.Logger.LogInformation($"Created product with ID: {product.Id}");
// Manual response construction
return new APIGatewayProxyResponse
{
StatusCode = 201,
Body = JsonSerializer.Serialize(product),
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" },
{ "Location", $"/products/{product.Id}" }
}
};
}
}
Understanding the Boilerplate
Notice the significant amount of manual work required with this traditional approach:
Manual Request Handling
- Path parameter extraction: You must manually extract values from
request.PathParameters - Body deserialization: JSON parsing requires explicit
JsonSerializer.Deserializecalls with error handling - Validation: Input validation must be implemented manually for each parameter
Manual Response Construction
- Status codes: Every response requires explicit
StatusCodeassignment - Headers: Content-Type and other headers must be set manually on every response
- Serialization: Response bodies require explicit
JsonSerializer.Serializecalls
Repetitive Patterns
Looking at both handlers, you'll notice repeated patterns:
- Error response construction is duplicated
- Header dictionaries are created multiple times
- JSON serialization/deserialization logic is scattered throughout
This boilerplate increases the chance of errors and makes the code harder to maintain.
AWS Configuration
serverless.template (AWS SAM)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Traditional Lambda Function Example
Resources:
GetProductFunction:
Type: AWS::Serverless::Function
Properties:
Handler: MyLambdaFunction::MyLambdaFunction.Functions::GetProduct
Runtime: dotnet8
MemorySize: 256
Timeout: 30
Events:
GetProduct:
Type: Api
Properties:
Path: /products/{id}
Method: GET
CreateProductFunction:
Type: AWS::Serverless::Function
Properties:
Handler: MyLambdaFunction::MyLambdaFunction.Functions::CreateProduct
Runtime: dotnet8
MemorySize: 256
Timeout: 30
Events:
CreateProduct:
Type: Api
Properties:
Path: /products
Method: POST
Next Steps
While this traditional approach works, it requires significant boilerplate code for every endpoint. The Lambda Annotations tutorial shows how to dramatically reduce this complexity using C# attributes.
Continue to: Lambda Annotations Tutorial →