A basic template to follow when creating an AWS Lambda function in dotnet/C# to add IConfiguration (from files and environment), create the DI container with basic AWS/Lambda integration wired up.
Function.cs (below example shows SQS integration, change accordingly):
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace YourNS;
public class Function()
{
private static readonly IServiceProvider ServiceProvider = new Startup().BuildServiceProvider();
public async Task FunctionHandler(SQSEvent evnt, ILambdaContext _)
{
using var scope = ServiceProvider.CreateScope();
var processor = scope.ServiceProvider.GetRequiredService<IProcessor>();
await processor.ProcessAsync(evnt);
}
}
The service provider is stored as static, which provides the closest thing to a "Singleton" scope for your DI services. When AWS re-uses a lambda instance you'll get some benefit to keeping your singletons alive. The creation of a "scope" at the top of the dependency tree for processing the requests adds the ability to use scoped/transient DI services "per request" (for SQS you might also choose to create a scope for each Record in the event, if you want more granularity).
Startup.cs:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup()
{
var currentDirectory = Directory.GetCurrentDirectory();
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(currentDirectory)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environment}.json", true)
.AddEnvironmentVariables();
Configuration = configurationBuilder.Build();
}
public IServiceProvider BuildServiceProvider()
{
var serviceCollection = new ServiceCollection();
var options = Configuration.GetAWSOptions();
serviceCollection.AddDefaultAWSOptions(options);
serviceCollection.AddLogging(builder =>
{
builder.AddLambdaLogger(new LambdaLoggerOptions(Configuration));
if (!Enum.TryParse<LogLevel>(Environment.GetEnvironmentVariable("AWS_LAMBDA_HANDLER_LOG_LEVEL") ?? "Information", out var absoluteMinimumLevel))
{
absoluteMinimumLevel = LogLevel.Information;
}
builder.SetMinimumLevel(absoluteMinimumLevel);
});
serviceCollection.AddTransient<IProcessor, Processor>();
return serviceCollection.BuildServiceProvider();
}
}
This class builds the IConfiguration instance from the various sources (add/remove as appropriate for your use-case). When the "BuildServiceProvider" method is called the DI container is setup, including wiring up the basic AWS/Lambda options and logging. The "IProcessor" is used as the entrypoint class called by FunctionHandler and becomes the top of the dependency graph for your application logic.
For more information about the logger setup and how to configure the log levels in your appsettings.json file, see my other blog post here
https://www.craigwardman.com/blog/dotnet-lambda-logger-levels