Use EF Core with AWS Lambda Functions

This is Part 3 in a 3 part series:

  1. Add .NET Core DI and Config Goodness to AWS Lambda Functions
  2. IDesignTimeDbContextFactory and Dependency Injection: A Love Story
  3. Use EF Core with AWS Lambda Functions (this post)

In a previous post I demonstrated how to set up Dependency Injection and Configuration for AWS Lambda Functions that are written in C# and use .NET Core.   The purpose of this post is to provide an example and some best practices for using Entity Framework Core with AWS Lambda.

Note: You can download or clone the code for this post here: https://github.com/tonysneed/net-core-lambda-di-config-ef

One of the benefits of adding DI and Config to a Lambda function is that you can abstract away persistence concerns from your application, allowing you greater flexibility in determining which concrete implementation to use. For example, you may start out with a relational data store (such as SQL Server, PostgreSQL or SQLite), but decide later to move to a NoSQL database or other non-relational store, such as Amazon S3. This is where the Repository Pattern comes in.

Note: If you’re going to get serious about using Repository and Unit of Work patterns, you should use a framework dedicated to this purpose, such as URF (Unit of Work and Repository Framework): https://github.com/urfnet/URF.Core

A Simple Example

Let’s start with a simple example, a Products repository. You would begin by defining an interface, for example, IProductRepository. Notice in the code below that the GetProduct method returns a Task. This is so that IO-bound operations can execute without blocking the calling thread.

public interface IProductRepository
{
Task<Product> GetProduct(int id);
}

You’re going to want to place this interface in a .NET Standard class library so that it can be referenced separately from specific implementations. Then create a ProductRepository class that implements IProductRepository. This can go in a .NET Standard class library that includes a package reference to an EF Core provider, for example, Microsoft.EntityFrameworkCore.SqlServer. You will want to add a constructor that accepts a DbContext-derived class.

public class ProductRepository : IProductRepository
{
public SampleDbContext Context { get; }
public ProductRepository(SampleDbContext context)
{
Context = context;
}
public async Task<Product> GetProduct(int id)
{
return await Context.Products.SingleOrDefaultAsync(e => e.Id == id);
}
}

Dependency Resolution

At this point you’re going to want to add a .NET Standard class library that can be used for dependency resolution. This is where you’ll add code that sets up DI and registers services that are used by classes in your application, including the DbContext that is used by your ProductRepository.

public class DependencyResolver
{
public IServiceProvider ServiceProvider { get; }
public string CurrentDirectory { get; set; }
public Action<IServiceCollection> RegisterServices { get; }
public DependencyResolver(Action<IServiceCollection> registerServices = null)
{
// Set up Dependency Injection
var serviceCollection = new ServiceCollection();
RegisterServices = registerServices;
ConfigureServices(serviceCollection);
ServiceProvider = serviceCollection.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
// Register env and config services
services.AddTransient<IEnvironmentService, EnvironmentService>();
services.AddTransient<IConfigurationService, ConfigurationService>
(provider => new ConfigurationService(provider.GetService<IEnvironmentService>())
{
CurrentDirectory = CurrentDirectory
});
// Register DbContext class
services.AddTransient(provider =>
{
var configService = provider.GetService<IConfigurationService>();
var connectionString = configService.GetConfiguration().GetConnectionString(nameof(SampleDbContext));
var optionsBuilder = new DbContextOptionsBuilder<SampleDbContext>();
optionsBuilder.UseSqlServer(connectionString, builder => builder.MigrationsAssembly("NetCoreLambda.EF.Design"));
return new SampleDbContext(optionsBuilder.Options);
});
// Register other services
RegisterServices?.Invoke(services);
}
}

There are parts of this class that are worthy of discussion. First, notice that the constructor accepts a delegate for registering services. This is so that classes using DependencyResolver can pass it a method for adding other dependencies. This is important because the application will register dependencies that are of no interest to the EF Core CLI.

Another thing to point out is the code that sets the CurrentDirectory of the ConfigurationService. This is required in order to locate the appsettings.*.json files residing at the root of the main project.

Lastly, there is code that registers the DbContext with the DI system. This calls an overload of AddTransient that accepts an IServiceProvider, which is used to get an instance of the ConfigurationService that supplies a connection string to the UseSqlServer method of the DbContextOptionsBuilder. This code can appear somewhat obtuse if you’re not used to it, but the idea is to use the IServiceProvider of the DI system to resolve services that are required for passing additional parameters to constructors.

To use DI with a Lambda function, simply add a constructor to the Function class that creates a DependencyResolver, passing a ConfigureServices method that registers IProductRepository with the DI system. The FunctionHandler method can then use the repository to retrieve a product by id.

public class Function
{
// Repository
public IProductRepository ProductRepository { get; }
public Function()
{
// Get dependency resolver
var resolver = new DependencyResolver(ConfigureServices);
// Get products repo
ProductRepository = resolver.ServiceProvider.GetService<IProductRepository>();
}
// Use this ctor from unit tests that can mock IProductRepository
public Function(IProductRepository productRepository)
{
ProductRepository = productRepository;
}
/// <summary>
/// A simple function that takes an id and returns a product.
/// </summary>
/// <param name="input"></param>
/// <param name="context"></param>
/// <returns></returns>
public async Task<Product> FunctionHandler(string input, ILambdaContext context)
{
int.TryParse(input, out var id);
if (id == 0) return null;
return await ProductRepository.GetProduct(id);
}
// Register services with DI system
private void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IProductRepository, ProductRepository>();
}
}
view raw FunctionEf.cs hosted with ❤ by GitHub

Notice the second constructor that accepts an IProductRepository. This is to support unit tests that pass in a mock IProductRepository. For example, here is a unit test that uses Moq to create a fake IProductRepository. This allows for testing logic in the FunctionHandler method without connecting to an actual database, which would make the test fragile and slow.

[Fact]
public async void Function_Should_Return_Product_By_Id()
{
// Mock IProductRepository
var expected = new Product
{
Id = 1,
ProductName = "Chai",
UnitPrice = 10
};
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(m => m.GetProduct(It.IsAny<int>())).ReturnsAsync(expected);
// Invoke the lambda function and confirm correct value is returned
var function = new Function(mockRepo.Object);
var result = await function.FunctionHandler("1", new TestLambdaContext());
Assert.Equal(expected, result);
}
}
view raw FunctionTest.cs hosted with ❤ by GitHub

EF Core CLI

In a previous post I proposed some options for implementing an interface called IDesignTimeDbContextFactory, which is used by the EF Core CLI for to create code migrations and apply them to a database.

aws-unicorn-framed.png

This allows the EF Core tooling to retrieve a connection string from the appsettings.*.json file that corresponds to a specific environment (Development, Staging, Production, etc).

"ConnectionStrings": {
"SampleDbContext": "Data Source=(localdb)\\MsSqlLocalDb;initial catalog=SampleDb;Integrated Security=True; MultipleActiveResultSets=True"
}
"ConnectionStrings": {
"SampleDbContext": "Data Source=sample-instance.xxx.eu-west-1.rds.amazonaws.com;initial catalog=SampleDb;User Id=xxx;Password=xxx; MultipleActiveResultSets=True"
}

Here is a sample DbContext factory that uses a DependencyResolver to get a DbContext from the DI system.

public class SampleDbContextFactory : IDesignTimeDbContextFactory<SampleDbContext>
{
public SampleDbContext CreateDbContext(string[] args)
{
// Get DbContext from DI system
var resolver = new DependencyResolver
{
CurrentDirectory = Path.Combine(Directory.GetCurrentDirectory(), "../NetCoreLambda")
};
return resolver.ServiceProvider.GetService(typeof(SampleDbContext)) as SampleDbContext;
}
}

To set the environment, simply set the ASPNETCORE_ENVIRONMENT environment variable.

rem Set environment
set ASPNETCORE_ENVIRONMENT=Development
rem View environment
set ASPNETCORE_ENVIRONMENT
view raw set-env-win.cmd hosted with ❤ by GitHub

Then run the dotnet-ef commands to add a migration and create a database with a schema that mirrors entity definitions and their relationships. You’ll want to do this twice: once for the Development environment and again for Production.

rem Create EF code migration
dotnet ef migrations add initial
rem Apply migration to database
dotnet ef database update

Try It Out!

Once you have created the database, you can press F5 to launch the AWS.NET Mock Lambda Test Tool, which you can use to develop and debug your Lambda function locally. Simply enter a value of 1 for Function Input and click the Execute Function button.

mock-lambda-test-tool.png

You should see JSON for Product 1 from the database.

mock-lambda-test-tool-result.png

When you’re confident everything works locally, you can throw caution to the wind and upload your Lambda function to AWS.

upload-lambda-function1.png

Make sure that the ASPNETCORE_ENVIRONMENT environment variable is set appropriately.

upload-lambda-function2.png

You can then bravely execute your deployed Lambda function.

execute-lambda

Conclusion

One of the benefits of using C# for AWS Lambda functions is built-in support for Dependency Injection, which is a first-class citizen in .NET Core and should be as indispensable to developers as a Jedi’s light saber. The tricky part can be setting up DI so that it can be used both at runtime by the Lambda function and at development-time by the EF Core CLI. With the knowledge you now possess, you should have no trouble implementing a microservices architecture with serverless functions that are modular and extensible. Cheers!

About Tony Sneed

Sr. Software Solutions Architect, Hilti Global Application Software
This entry was posted in Technical and tagged , , , . Bookmark the permalink.

2 Responses to Use EF Core with AWS Lambda Functions

  1. sum says:

    my pleasure to follow the guidance which is clear and overly enjoyable 🙂
    thank you

  2. andres jerez says:

    Hi Tony, your tutorial is amazing, im about to apply this in a personal project and i got one question, if I need to deploy 10 different functions should I do it one by one? or there is a way to do it massively?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.