Skip to content

cythral/lambdajection

Repository files navigation

Lambdajection

Nuget Nuget GitHub GitHub Workflow Status Codecov Sponsor on Github Donate on Paypal

Write elegant and testable AWS Lambdas + Lambda-backed custom resources using C#/.NET and Microsoft's extensions for dependency injection, configuration and more. Start from a template, add handler code, inject services and let Lambdajection do the rest!

Why Lambdajection?

  • Easy dependency management: configure dependencies/services using a Startup class, similar to how it is done in ASP.NET Core.
  • Easy secrets management: automatically decrypt options marked as [Encrypted] using KMS or your own provided cryptography service.
  • Faster startup times: we're using code generation over reflection wherever possible to help minimize cold-start times.
  • Highly configurable: customize serialization, configuration, and run custom code before your main handler is invoked.
  • Highly testable: facilitates use of dependency-injection to make testing your Lambda easier.
  • Flexibility: you can use AWS' provided runtime or roll your own runtime containing .NET. Lambdajection works both ways, and can even generate code for running on a custom/provided runtime.

Community contribution/pull requests are welcome and encouraged! See the contributing guide for instructions. Report issues on JIRA - you can report anonymously or include github username/contact info on the ticket.

Table of Contents

1. Installation

1.1. Metapackage

See the packages section for a list of available packages. Starting in v0.5.0-beta2, you will need to have the .NET 5 SDK installed.

dotnet add package Lambdajection

1.2. Templates

dotnet new -i Lambdajection.Templates

1.3. Development Builds

Development builds are generated for PRs and uploaded to GitHub Packages. To use them, update the user config file for nuget (varies by OS - see this article) and add this to the packageSourceCredentials section of that file:

<github>
    <add key="Username" value="USERNAME" />
    <add key="ClearTextPassword" value="TOKEN" />
</github>

Replace USERNAME with your username, and TOKEN with a personal access token from GitHub that has permissions to read packages. It is important that this goes in the user config file rather than the project one, so that you do not accidentally leak your personal access token to the world.

Then, in your project's nuget.config file, add the following to your packageSources section:

<add key="github" value="https://nuget.pkg.github.com/cythral/index.json" />

Finally, you may use development builds by adding the package and version to your .csproj, for instance:

<PackageReference Include="Lambdajection" Version="0.3.0-gc2ca768d3f" />

Browse development builds here.

2. Packages

Lambdajection Nuget Nuget
Lambdajection.Attributes Nuget Nuget
Lambdajection.Core Nuget Nuget
Lambdajection.Generator Nuget Nuget
Lambdajection.Encryption Nuget Nuget
Lambdajection.Templates Nuget Nuget
Lambdajection.Runtime Nuget Nuget
Lambdajection.Layer Nuget Nuget
Lambdajection.Framework Nuget Nuget
Lambdajection.Framework.BuildTime Nuget Nuget
Lambdajection.CustomResource Nuget Nuget
Lambdajection.CustomResource.BuildTime Nuget Nuget

3. Templates

Lambdajection .NET templates are available for your convenience. Run dotnet new [template-name] --help to see a list of available options for each template.

Lambdajection Project

dotnet new lambdajection

Creates a new C# project with Lambdajection installed, plus boilerplate for a Lambda Handler and Startup class.

Options Class

dotnet new lambdajection-options

Creates a new Options class to be injected into your Lambda as an IOption<>.

4. Usage

4.1. Lambda Handler

Writing the lambda is simple: Just define a public, partial class that contains a Handle method and annotate the class with the Lambda attribute. The Lambda attribute requires that you specify a startup class - more on this in the next step. You are not limited to an request/input parameter of type object - this can be any serializable value or reference type. Same goes for the return value, however the return value must be enclosed in a Task.

using System.Threading;
using System.Threading.Tasks;

using Amazon.Lambda.Core;

using Lambdajection.Attributes;

namespace Your.Namespace
{
    [Lambda(typeof(Startup))]
    public partial class YourLambda
    {
        private IRegisteredService yourService;

        public YourLambda(IRegisteredService yourService)
        {
            this.yourService = yourService;
        }

        public Task<object> Handle(object request, CancellationToken cancellationToken)
        {
            return new {
                foo = request
            };
        }
    }
}

4.2. Request Validation Attributes

Validation attributes in the System.ComponentModel.DataAnnotations namespace can be added to request properties, and they will be automatically evaluated against the request before your Lambda runs:

using System.ComponentModel.DataAnnotations;

class Request
{
    [Range(3, 12)]
    public int Age { get; set; }
}

4.3. Startup Class

The startup class configures services that are injected into the Lambda's IoC container / service collection.

  • Use the ConfigureServices method to add services to your lambda.
    • Use IServiceCollection.UseAwsService<IAmazonService> to inject AWS Clients and Client Factories into the lambda. See the example here.
  • Optionally use the ConfigureLogging method to configure additional log settings.
using System;

using Lambdajection.Core;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Your.Namespace
{
    public class Startup : ILambdaStartup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            this.Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // configure injected services here
            services.AddScoped<IRegisteredService, DefaultRegisteredService>();

            // Add AWS Services by their interface name - the default
            services.UseAwsService<IAmazonService>();
        }

        public void ConfigureLogging(ILoggingBuilder logging)
        {
            // this method is optional
            // logging comes preconfigured to log to the console
        }
    }
}

4.4. Customizing Configuration

By default, configuration is environment variables-based. If you would like to use a file-based or other configuration scheme, you may supply a custom configuration factory to the Lambda attribute:

[Lambda(typeof(Startup), ConfigFactory = typeof(ConfigFactory))]
public partial class Lambda
{
    ...

A custom config factory might look like the following:

using System.IO;

using Lambdajection.Core;

using Microsoft.Extensions.Configuration;

namespace Your.Namespace
{
    public class ConfigFactory : ILambdaConfigFactory
    {
        public IConfigurationRoot Create()
        {
            return new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true)
                .Build();
        }
    }
}

See the full example here.

4.5. Adding Options

You can add an options section by defining a class for that section, and annotating it with the LambdaOptions attribute. If any options are in encrypted form, add the Encrypted attribute to that property. When the options are requested, the IDecryptionService singleton in the container will be used to decrypt those properties. The default decryption service uses KMS to decrypt values.

  • The Encrypted attribute, IDecryptionService and DefaultDecryptionService are all provided by the Lambdajection.Encryption package.
  • Option classes must be in the same assembly as your lambda.
  • You can replace the default decryption service with your own IDecryptionService by injecting it as a singleton in your Startup class' ConfigureServices method.
  • See the example for using Encrypted properties.
using Lambdajection.Encryption;

namespace Your.Namespace
{
    [LambdaOptions(typeof(LambdaHandler), "SectionName")]
    public class ExampleOptions
    {
        public string ExampleValue { get; set; }

        [Encrypted]
        public string ExampleEncryptedValue { get; set; }
    }
}

4.6. Initialization Services

Initialization services can be used to initialize data or perform some task before the lambda is run. Initialization services should implement ILambdaInitializationService and be injected into the container as singletons at startup.

4.7. Disposers

Disposers can be used to cleanup unmanaged resources, such as open file-handles and network connections. Lambdajection supports Lambdas that implement either IDisposable, IAsyncDisposable or both. If you implement both, DisposeAsync will be preferred.

4.8. Handler Scheme

When configuring your lambda on AWS, the method name you'll want to use will be Run (NOT Handle). For context, Run is a static method generated on your class during compilation. It takes care of setting up the IoC container, if it hasn't been setup already.

So, going off the example above, the handler scheme would look like this:

Your.Assembly.Name::Your.Namespace.YourLambda::Run

You can customize the name of the "Run" method via the RunnerMethod property of LambdaAttribute.

4.9. Lambda-Backed Custom Resource Providers

Lambdajection also allows you to write lambda-backed custom resource providers. Just write create, update and delete methods and Lambdajection will take care of deciding which one gets called + respond to CloudFormation.

Just use the Custom Resource Provider Attribute:

using System.Threading.Tasks;

using Lambdajection.Attributes;
using Lambdajection.CustomResource;

namespace Your.Namespace
{
    [CustomResourceProvider(typeof(Startup))]
    public class Handler
    {
        public Task<Response> Create(CustomResourceRequest<Properties> request, CancellationToken cancellationToken)
        {
            ...
        }

        public Task<Response> Update(CustomResourceRequest<Properties> request, CancellationToken cancellationToken)
        {
            ...
        }

        public Task<Response> Delete(CustomResourceRequest<Properties> request, CancellationToken cancellationToken)
        {
            ...
        }
    }

    public class Response : ICustomResourceOutputData
    {
        public string Id { get; set; } // Id becomes the custom resource's PhysicalResourceId.

        /* Any other properties you define here become attributes of the custom resource
        
        ie. !GetAtt Resource.YourProperty */
    }

    public class Properties
    {
        /* If the name changes, the Create method will be called instead of update, since the UpdateRequiresReplacement attribute is used here. */
        [UpdateRequiresReplacement]
        public string Name { get; set; }
    }
}

See also the custom resource example.

4.10. Custom Runtimes

Lambdajection can be used with custom runtimes starting in v0.5.0-beta2, that way you can use a newer version of .NET as soon as it comes out, even if it is not an LTS release.

In order to use custom runtimes, add the Lambdajection.Runtime package to your csproj file. You must also specify the RuntimeIdentifiers property, with at least linux-x64 included:

<Project>
    <PropertyGroup>
        <RuntimeIdentifiers>linux-x64</RuntimeIdentifiers>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Lambdajection" Version="$(LambdajectionVersion)" />
        <PackageReference Include="Lambdajection.Runtime" Version="$(LambdajectionVersion)" />
    </ItemGroup>
</Project>

You may also optionally set the SelfContained property:

  • Set it to true if you want to deploy as a self-contained package. In this case, Lambdajection will automatically set your assembly name to bootstrap.
  • Set it to false if you want to deploy as a framework dependent package, ie you installed .NET to a Lambda Layer and want to use that to cut-down on deployment package sizes. In this case, your assembly name will remain unchanged.
  • In both cases, a main method / program entrypoint will be generated for you with the aid of Amazon.Lambda.RuntimeSupport.

See an example of a non-self contained lambda using a custom runtime here. (Example documentation coming soon). Note that the example is using a Lambda Layer we deployed with a custom bootstrap file.

4.11. Lambda Layer

You can use a Lambda Layer containing Lambdajection and all of its dependencies to cut down on package sizes. The layer will be available on the serverless application repository. Once deployed, you can use it on functions that use the Lambdajection.Runtime package on custom runtimes containing .NET 5.

To use the layer:

  1. Deploy lambdajection-layer from the Serverless Application Repository to your AWS Account.
    • You must use the same semantic version as the Lambdajection package in your project.
  2. Note the value of the LayerArn output in the resulting stack and add it to your Lambda's list of layers. See the custom runtime example template on how to do this - specifically the CustomRuntime resource's Layers section.
  3. Add the Lambdajection.Runtime and Lambdajection.Layer packages to your project (run dotnet add package Lambdajection.Layer).
    • Make sure to use the same version as the Lambdajection package in your project.
  4. Finally, you will need to set the DOTNET_SHARED_STORE environment variable to /opt/. This is because Lambda Layers are unzipped to that directory, and .NET needs to know where to look for the runtime package store.

5. Examples

6. Acknowledgments

  1. CodeGeneration.Roslyn - Was used for compile-time code generation using attributes in versions v0.1.0 - v0.4.0. Newer versions use .NET 5 Source Generators.
  2. Simple Lambda Dependency Injection in AWS Lambda .NET Core by Gary Woodfine - primary inspiration for this project.

7. Donations

If you use this project and find it useful, please consider donating. Donations are accepted via Github Sponsors and PayPal.

8. Contributing

Issues and feature requests may be reported anonymously on JIRA Cloud. Pull requests are always welcome! See the contributing guide for more information.

9. Security

Security issues can be reported on our JIRA. See our security policy for more information.

10. License

This project is licensed under the MIT License.