Skip to main content

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.

Why Lambda Annotations?

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

What Lambda Annotations Provides

Compared to the traditional approach, Lambda Annotations offers:

Traditional ApproachLambda Annotations
Manual path parameter extractionAutomatic parameter binding via [FromRoute]
Manual body deserializationAutomatic JSON deserialization via [FromBody]
Manual response constructionDirect return of objects or IHttpResult
Explicit status codes on every responseDeclarative [HttpApi] attributes
Scattered JSON serializationAutomatic 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
PackagePurpose
Amazon.Lambda.CoreCore Lambda interfaces
Amazon.Lambda.AnnotationsAttribute-based Lambda development
Amazon.Lambda.Serialization.SystemTextJsonJSON 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

  1. [FromRoute] - Automatically extracts id from the URL path
  2. [FromBody] - Automatically deserializes the request body to your model
  3. Direct return - Return your object directly; serialization is automatic
  4. IHttpResult - Use HttpResults.Created(), HttpResults.NotFound(), etc. for specific status codes
  5. 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.template is 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 →

Additional Resources