Lambda Annotations for Cleaner C# Functions
Lambda Annotations is an AWS SDK feature that dramatically simplifies Lambda function development using C# attributes. Instead of manually handling request parsing and response construction, you declare your intent through attributes and let the framework handle the rest.
Lambda Annotations eliminates the boilerplate shown in the Traditional Lambda Functions tutorial. You'll write less code, reduce errors, and focus on business logic instead of infrastructure concerns.
Prerequisites
- Completed the Traditional Lambda Functions tutorial (recommended)
- .NET 8 SDK or later
- AWS account with Lambda access
What Lambda Annotations Provides
Compared to the traditional approach, Lambda Annotations offers:
| Traditional Approach | Lambda Annotations |
|---|---|
| Manual path parameter extraction | Automatic parameter binding via [FromRoute] |
| Manual body deserialization | Automatic JSON deserialization via [FromBody] |
| Manual response construction | Direct return of objects or IHttpResult |
| Explicit status codes on every response | Declarative [HttpApi] attributes |
| Scattered JSON serialization | Automatic serialization |
Project Setup
Create a New Lambda Project
dotnet new classlib -n MyAnnotatedLambda
cd MyAnnotatedLambda
Install Required NuGet Packages
dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.Annotations
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
| Package | Purpose |
|---|---|
Amazon.Lambda.Core | Core Lambda interfaces |
Amazon.Lambda.Annotations | Attribute-based Lambda development |
Amazon.Lambda.Serialization.SystemTextJson | JSON serialization |
Code Example
Here's the same product API from the traditional tutorial, now using Lambda Annotations:
The Product Model
namespace MyAnnotatedLambda.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 Functions with Annotations
using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.APIGateway;
using MyAnnotatedLambda.Models;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyAnnotatedLambda;
public class Functions
{
/// <summary>
/// Get a product by ID
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/products/{id}")]
public Product GetProduct(
[FromRoute] string id,
ILambdaContext context)
{
context.Logger.LogInformation($"Getting product with ID: {id}");
// Simulate fetching product (in real app, this would query a database)
return new Product
{
Id = id,
Name = "Sample Product",
Price = 29.99m,
Description = "A sample product for demonstration"
};
}
/// <summary>
/// Create a new product
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Post, "/products")]
public IHttpResult CreateProduct(
[FromBody] Product product,
ILambdaContext context)
{
// Assign an ID to the new product
product.Id = Guid.NewGuid().ToString();
context.Logger.LogInformation($"Created product with ID: {product.Id}");
return HttpResults.Created($"/products/{product.Id}", product);
}
}
Comparing the Code
Let's see the dramatic reduction in boilerplate:
GetProduct: Traditional vs Annotations
Traditional (35+ lines):
public APIGatewayProxyResponse GetProduct(
APIGatewayProxyRequest request,
ILambdaContext context)
{
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" } }
};
}
// ... more code ...
return new APIGatewayProxyResponse
{
StatusCode = 200,
Body = JsonSerializer.Serialize(product),
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
With Annotations (12 lines):
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/products/{id}")]
public Product GetProduct([FromRoute] string id, ILambdaContext context)
{
context.Logger.LogInformation($"Getting product with ID: {id}");
return new Product
{
Id = id,
Name = "Sample Product",
Price = 29.99m,
Description = "A sample product for demonstration"
};
}
Key Differences
[FromRoute]- Automatically extractsidfrom the URL path[FromBody]- Automatically deserializes the request body to your model- Direct return - Return your object directly; serialization is automatic
IHttpResult- UseHttpResults.Created(),HttpResults.NotFound(), etc. for specific status codes- No manual headers - Content-Type is handled automatically
Handling Errors
Lambda Annotations provides IHttpResult for cases where you need specific HTTP responses:
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/products/{id}")]
public IHttpResult GetProduct([FromRoute] string id, ILambdaContext context)
{
if (string.IsNullOrEmpty(id))
{
return HttpResults.BadRequest("Product ID is required");
}
var product = FindProduct(id); // Your database lookup
if (product == null)
{
return HttpResults.NotFound($"Product {id} not found");
}
return HttpResults.Ok(product);
}
Source Generation
Lambda Annotations uses C# source generators to create the boilerplate code at compile time. This means:
- No runtime reflection - Better cold start performance
- Compile-time validation - Catch configuration errors early
- Auto-generated CloudFormation - The
serverless.templateis generated for you
After building, check the Generated folder in your project to see the generated code.
Next Steps
Now that you've seen how Lambda Annotations reduces boilerplate, the next step is adding OpenAPI documentation to your API. The OpenAPI Generation tutorial shows how LambdaOpenApi automatically generates OpenAPI specifications from your annotated functions.
Continue to: OpenAPI Generation Tutorial →