This is Part 3 in a 3 part series:
- Add .NET Core DI and Config Goodness to AWS Lambda Functions
- IDesignTimeDbContextFactory and Dependency Injection: A Love Story
- 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>(); | |
} | |
} |
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); | |
} | |
} |
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.
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 |
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.
You should see JSON for Product 1 from the database.
When you’re confident everything works locally, you can throw caution to the wind and upload your Lambda function to AWS.
Make sure that the ASPNETCORE_ENVIRONMENT
environment variable is set appropriately.
You can then bravely execute your deployed Lambda function.
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!
my pleasure to follow the guidance which is clear and overly enjoyable 🙂
thank you
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?