Skip to main content

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.

Important

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
PackagePurpose
Amazon.Lambda.CoreCore Lambda interfaces and attributes
Amazon.Lambda.APIGatewayEventsRequest/response types for API Gateway integration
Amazon.Lambda.Serialization.SystemTextJsonJSON 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.Deserialize calls with error handling
  • Validation: Input validation must be implemented manually for each parameter

Manual Response Construction

  • Status codes: Every response requires explicit StatusCode assignment
  • Headers: Content-Type and other headers must be set manually on every response
  • Serialization: Response bodies require explicit JsonSerializer.Serialize calls

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 →

Additional Resources