.NET on AWS Blog
Bob’s Used Books: Build a .NET Serverless Application on AWS – Part 3: Infrastructure as Code and Development Patterns
Introduction
Infrastructure as Code and development patterns reduce common challenges when building .NET serverless applications. Managing infrastructure and debugging distributed serverless applications can slow down .NET development. Unfamiliar patterns and boilerplate code make debugging difficult in distributed systems.
The first post in this series covered the deployment steps for Bob’s Used Books serverless application. The second post provided an architecture overview with diagrams. Reviewing it will help you understand the implementation patterns discussed here. Bob’s Used Books is a serverless bookstore application built with .NET 8 that uses Amazon API Gateway, AWS Lambda, Amazon DynamoDB, Amazon Cognito, and Amazon Verified Permissions. The architecture uses event-driven patterns with separate stacks for authentication and business logic.
In this post, you will learn how to build a maintainable and observable .NET serverless applications using the AWS Cloud Development Kit (AWS CDK) with C#. You will discover how three key frameworks directly address serverless development challenges by eliminating boilerplate code, providing type-safe infrastructure definitions, and adding production-ready observability:
- AWS Cloud Development Kit (CDK): Define infrastructure in C# with type safety and IntelliSense.
- Lambda Annotations Framework: Write Lambda functions with declarative attributes.
- Lambda Powertools: Add observability without boilerplate code.
View the complete source code for bobs-used-bookstore-serverless.
AWS Cloud Development Kit (CDK) for .NET
Developers can define infrastructure using familiar C# syntax with the AWS Cloud Development Kit (CDK), bringing the full power of modern programming to cloud provisioning. By using C#, developers at Bob’s Used Books gain access to robust features such as type safety, IntelliSense, and built-in refactoring support directly in their IDE. This approach creates maintainable and expressive infrastructure definitions.
In Bob’s Used Books serverless, the AWS CDK for .NET defines the infrastructure resources. The following code shows how to define a Lambda function with its runtime, handler, and environment variables.
var myFunction = new Function(this, "BookInventoryFunction", new FunctionProps
{
Runtime = Runtime.DOTNET_8,
Handler = "BookInventory::BookInventory.Functions::GetBook",
Code = Code.FromAsset("./src/BookInventory"),
Environment = new Dictionary<string, string>
{
["TABLE_NAME"] = booksTable.TableName
}
});
This C# code defines a Lambda function with its runtime, handler, and environment variables. Developers get IntelliSense support, compile-time type checking, and the ability to refactor infrastructure code just like application code. The CDK approach supports unit testing, code reuse, and version control for infrastructure definitions.
CDK Stack Architecture
The Bob’s Used Books CDK implementation uses a separation of concerns approach through independent stacks. Managing authentication, business logic, and shared infrastructure in a single stack creates deployment bottlenecks and ownership conflicts.
The solution uses two distinct stacks.
- AuthenticationStack: Handles user identity through Amazon Cognito and stores configuration in AWS Systems Manager Parameter Store.
- BookInventoryServiceStack: Retrieves authentication configuration and builds the API layer with Lambda functions, API Gateway, and DynamoDB.
This separation enables independent deployment of authentication changes from business logic updates. The stacks communicate through Parameter Store, creating loose coupling.

Figure 1: CDK stack architecture
Shared Constructs Pattern
AWS CDK constructs are reusable infrastructure components that encapsulate recommended practices and organizational standards. Rather than duplicating infrastructure code across projects, constructs promote consistency and maintainability.
Bob’s Used Books serverless demonstrates this pattern with a reusable LambdaFunction construct that encapsulates common Lambda configuration.
public class LambdaFunction : Construct
{
public Function Function { get; }
public LambdaFunction(Construct scope, string id, LambdaFunctionProps props)
: base(scope, id)
{
this.Function = new DotNetFunction(this, id, new DotNetFunctionProps()
{
// Use .NET 8 runtime for each Lambda functions
Runtime = Runtime.DOTNET_8,
// Enable X-Ray tracing for distributed request tracking and performance analysis
Tracing = Tracing.ACTIVE,
// Automatically select ARM64 or x86_64 architecture based on build environment
Architecture = RuntimeInformation.ProcessArchitecture == Architecture.Arm64
? Architecture.ARM_64 : Architecture.X86_64,
// Configure Dead Letter Queue (DLQ) to capture failed invocations for debugging
OnFailure = new SqsDestination(new Queue(this, $"{id}FunctionDLQ"))
});
}
}
This construct creates every Lambda function in the application with consistent configuration without repeating those properties each time. Each function receives .NET 8 runtime, 512 MB of memory, 30-second timeout, and active AWS X-Ray tracing.
This pattern keeps infrastructure code DRY (Don’t Repeat Yourself), reduces configuration errors, and simplifies implementing organizational standards.
Amazon API Gateway Configuration with Fluent Interface
Defining API Gateway endpoints in CDK becomes repetitive when creating resources, methods, integrations, authorizers, and response models for each path and HTTP verb. A Fluent interface addresses this by chaining method calls together, resulting in code that reads more naturally and reduces boilerplate.
In Bob’s Used Books serverless, a custom Api construct implements a fluent interface for configuring REST API endpoints. The construct automatically handles resource creation, method setup, Lambda integrations, optional authorizer attachment, and standardized response formatting.
Here is how the main API is defined in the CDK stack:
var api = new Api(this, "BookInventoryApi")
.WithCognito(authorizerFunction)
.WithEndpoint("/books/{id}", HttpMethod.Get, getBookFunction, false)
.WithEndpoint("/books", HttpMethod.Post, addBookFunction)
.WithEndpoint("/books", HttpMethod.Get, listBooksFunction, false);
This chain of method calls creates a complete API Gateway configuration with proper authorization, standardized response codes, and consistent error handling.
Development Environment Isolation
Multiple developers working in the same AWS account for development create resource conflicts. Each developer needs an isolated environment to test changes without affecting other developers. The Bob’s Used Books implementation uses the postfix pattern to create developer-specific environments.
Appending a developer-specific suffix to all resource names gives each developer a completely isolated environment.
export STACK_POSTFIX="-dev-abc"
cdk deploy "AuthenticationStack${STACK_POSTFIX}"
This creates resources like BookStoreUserPool-dev-abc and BookInventory-dev-abc, providing complete isolation without complex tooling. The cleanup strategy uses environment-specific removal policies.
RemovalPolicy = string.IsNullOrWhiteSpace(postfix)
? RemovalPolicy.RETAIN // Production: preserve data
: RemovalPolicy.DESTROY // Development: allow cleanup
Development environments use RemovalPolicy.DESTROY for complete teardown with a single command. Production environments use RemovalPolicy.RETAIN for data protection. This approach prevents accidental data loss in production and simplifies development environment management.
Cross-Stack Communication with Parameter Store
The postfix pattern requires a solution for stack communication when resource names are dynamic. AWS Systems Manager Parameter Store serves as a configuration bridge between stacks.
The AuthenticationStack stores its configuration in Parameter Store with environment-specific parameter names.
new StringParameter(this, "UserPoolIdParameter", new StringParameterProps
{
ParameterName = $"/bookstore{postfix}/cognito/userpool-id",
StringValue = userPool.UserPoolId
});
The BookInventoryServiceStack retrieves this configuration at deployment time.
var userPoolId = StringParameter.ValueFromLookup(this,
$"/bookstore{postfix}/cognito/userpool-id");
This creates loose coupling that supports both isolated development and production deployments without hardcoded dependencies.
AWS Lambda Development Patterns
AWS Lambda Annotations Framework and AWS Lambda Powertools for .NET bring familiar ASP.NET Core-style patterns to serverless development. These libraries reduce boilerplate code and add production-ready observability capabilities.
Note: The Lambda Annotations Framework generates an AWS SAM (Serverless Application Model) template as part of its build process. In this solution, AWS CDK manages the infrastructure and doesn’t use the generated SAM file for deployment. Therefore, you can safely ignore it or exclude it from source control.
Declarative Lambda Functions with Annotations
The AWS Lambda Annotations Framework uses attributes to define Lambda function behavior. This declarative approach reduces boilerplate code.
The following code shows how annotations handle HTTP method mapping, parameter binding, and tracing configuration:
[LambdaFunction]
[RestApi(LambdaHttpMethod.Get, "/books")]
[Tracing(CaptureMode = TracingCaptureMode.Error)]
[Logging(ClearState = true)]
public async Task<APIGatewayProxyResponse> ListBooks(
[FromQuery] int pageSize = 10,
[FromQuery] string cursor = null)
{
cursor.AddObservabilityTag("ListBooks");
var response = await this.bookInventoryService.ListAllBooksAsync(pageSize, cursor);
return ApiGatewayResponseBuilder.Build(HttpStatusCode.OK, response);
}
The annotations handle HTTP method mapping, parameter binding, and tracing configuration. The function body focuses on business logic. This pattern reduces code complexity and improves maintainability.
Key benefits of the Annotations Framework
- Type-safe parameter binding: [FromQuery] attributes automatically parse and validate query parameters.
- Automatic serialization: Return types convert to JSON responses without manual serialization code.
- Integrated observability: [Tracing] and [Logging] attributes configure AWS X-Ray and Amazon CloudWatch Logs.
- Familiar syntax: Attributes mirror ASP.NET Core conventions, reducing the learning curve.
Structured Observability with AWS Lambda Powertools
AWS Lambda Powertools for .NET provides structured logging, custom metrics, and distributed tracing capabilities. It integrates with CloudWatch Logs, CloudWatch Metrics, and AWS X-Ray through simple annotations, eliminating manual setup.
Custom extensions correlate data across CloudWatch Logs and X-Ray:
public static void AddObservabilityTag(this string value, string tag)
{
Logger.AppendKey(tag, value); // Structured logging
Tracing.AddAnnotation(tag, value); // X-Ray tracing
}
This extension maintains the same contextual information in both CloudWatch Logs and AWS X-Ray traces. The same identifiers in both logs and traces simplify issue tracking across systems. Use custom extensions to correlate specific business context across multiple observability systems. For standard request tracking, the built-in correlation IDs provided by Lambda Powertools are sufficient.
Lambda Powertools provides several key observability capabilities:
- Structured logging: JSON-formatted logs with consistent fields for flexible querying.
- Correlation IDs: Automatic request tracking across distributed services.
- Custom metrics: Business metrics published to CloudWatch without API calls.
- Trace annotations: Searchable metadata in X-Ray for performance analysis.
- Cold-start tracking: Automatic detection and logging of Lambda cold-starts.
The framework handles AWS service integration. Developers focus on what to observe, not how to implement observability.
API Security Implementation with CDK
Security in serverless applications requires careful integration of authentication and authorization services. The Bob’s Used Books CDK implementation provisions Amazon Cognito for authentication and Amazon Verified Permissions for fine-grained authorization. This section shows how CDK code defines security infrastructure alongside application resources.
Cognito User Pool Configuration
The AuthenticationStack establishes user identity management through Amazon Cognito User Pool configuration.
var userPool = new UserPool(this, "BookStoreUserPool", new UserPoolProps
{
UserPoolName = $"BookStoreUserPool{postfix}",
SelfSignUpEnabled = true,
SignInAliases = new SignInAliases { Email = true },
AutoVerify = new AutoVerifiedAttrs { Email = true },
PasswordPolicy = new PasswordPolicy
{
MinLength = 8,
RequireLowercase = true,
RequireUppercase = true,
RequireDigits = true,
RequireSymbols = true
},
CustomAttributes = new Dictionary<string, ICustomAttribute>
{
["user_id"] = new StringAttribute(new StringAttributeProps { Mutable = false })
}
});
The configuration balances security with user experience. It requires email verification with strong passwords and supports self-service registration. Custom attributes like an immutable user_id support application-specific user tracking without exposing sensitive Cognito identifiers.
The CDK approach to security configuration offers several advantages.
- Version control: Security policies live in source control with full audit history.
- Consistency: Same security configuration across each environment.
- Testability: Validate security infrastructure before deployment.
- Documentation: Code serves as executable documentation of security requirements.
Amazon Verified Permissions Integration
The authorization layer uses Amazon Verified Permissions to define access policies in the Cedar policy language. Instead of embedding permission logic in application code, you define policies declaratively.
var policyStatement = $@"permit(
principal in BookInventoryApi::UserGroup::""{userGroupId}"",
action in [{actionsString}],
resource
);";
var policy = new CfnPolicy(this, $"{userGroupId}Policy", new CfnPolicyProps
{
Definition = new CfnPolicy.PolicyDefinitionProperty
{
Static = new CfnPolicy.StaticPolicyDefinitionProperty
{
Statement = policyStatement
}
},
PolicyStoreId = policyStore.AttrPolicyStoreId
});
This approach separates authorization concerns from business logic and provides audit trails and policy versioning. The CDK code provisions the policy store and policies, making authorization configuration part of the infrastructure deployment process.
Benefits of Cedar policy language:
- Human-readable policies: Authorization rules are clear and auditable.
- Fine-grained control: Policies specify exact actions on specific resources.
- Centralized management: Authorization logic lives in one place.
- Policy validation: Cedar validates policy syntax before deployment.
Lambda Authorizer Provisioning
The CDK code provisions the Lambda authorizer that bridges Cognito authentication with Verified Permissions authorization.
var authorizerFunction = new LambdaFunction(this, "AuthorizerFunction",
new LambdaFunctionProps
{
ProjectDir = "../src/BookInventory/BookInventory.Authorization",
Handler = "BookInventory.Authorization::BookInventory.Authorization.Functions_Authorize_Generated::Authorize",
Environment = new Dictionary<string, string>
{
["POLICY_STORE_ID"] = policyStore.AttrPolicyStoreId,
["USER_POOL_ID"] = userPoolId
}
});
// Grant permissions to call Verified Permissions
authorizerFunction.Function.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
Actions = new[] { "verifiedpermissions:IsAuthorized" },
Resources = new[] { policyStore.AttrArn }
}));
// Grant permissions to call Verified Permissions
authorizerFunction.Function.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
Actions = new[] { "verifiedpermissions:IsAuthorized" },
Resources = new[] { policyStore.AttrArn }
}));
The Lambda function validates JWT tokens, extracts user context, and calls the Verified Permissions service for policy evaluation. This architecture supports complex authorization scenarios and maintains performance through caching and efficient policy evaluation.
The authorizer pattern provides several operational benefits.
- Centralized authentication: Single point of token validation for each API endpoint.
- Policy caching: Authorization decisions cache for improved performance.
- Audit logging: Authorization decisions log to CloudWatch for compliance.
- Dynamic policies: Authorization rules update without code changes.
Conclusion
This post covered the Infrastructure as Code implementation and development patterns for Bob’s Used Books serverless application. AWS CDK, Lambda Annotations Framework, and Lambda Powertools work together to address common serverless development challenges such as infrastructure complexity, environment conflicts, deployment dependencies, and observability overhead.
These patterns provide value for building serverless applications that require consistent infrastructure standards and comprehensive observability. Organizations migrating from monolith .NET applications to serverless architectures benefit from the familiar .NET development patterns.
Developers start with the foundational elements like shared constructs, postfix environments, and stack separation, then expand to more complex scenarios. Start building your own serverless .NET applications today using these patterns. The combination of CDK, Lambda Annotations, and Lambda Powertools provides a production-ready foundation that scales from prototype to enterprise deployment.