Dependency Injection With Serverless Functions

Introduction

 
Dependency injection is a common design pattern used in .NET Core Web APIs. It provides a convenient mechanism to define a central repository of classes and configuration data required by controllers and other downstream dependent classes. Shifting to a serverless design moves away from the startup sequence of the Web API and the dependency injection scaffolding that developers are familiar with.
 
Serverless Functions provide a simple, single-purpose method signature. The HTTP Azure Serverless Function Template produces code that runs in response to an HTTP GET and HTTP POST; however, it lacks any perscriptive guidance for managing the configuration or dependency injection. The code below shows the default logic created when authoring an Azure Function using an HTTP Trigger.

  1. [FunctionName("Function1")]  
  2. public static async Task<IActionResult> Run(   
  3. [HttpTrigger(AuthorizationLevel.Function, "get""post", Route = null)] HttpRequest req, ILogger log)  
  4. {  
  5.   
  6.   log.LogInformation("C# HTTP trigger function processed a request.");  
  7.   
  8.   string name = req.Query["name"];  
  9.   
  10.   string requestBody = await new StreamReader(req.Body).ReadToEndAsync();  
  11.   
  12.   dynamic data = JsonConvert.DeserializeObject(requestBody);  
  13.   
  14.   name = name ?? data?.name;  
  15.   
  16.   return name != null ? (ActionResult)new OkObjectResult($"Hello, {name}")  
  17.       : new BadRequestObjectResult("Please pass a name on the query string or in the request body");  
  18.   
  19. }  

 This article shows how to apply the Microsoft.Extensions.DependencyInjection library to Azure Serverless Functions and AWS Lambdas. 

Bootstrapping the Serverless Function

 
ASP.NET Core Web API projects include a StartUp class that wires the configuration data, interfaces, and interface implementations into a dependency injection framework. As the class name implies, it fires when the Web API launches and steps through a bootstrapping pipeline process. The first request to the Web API kicks off the process while subsequent concurrent requests wait until the bootstrapping pipeline completes. Internally, the WebHost and WebHostBuilder establishes a lock and prevents multiple threads from executing the bootstrapping process.
 
Serverless Functions need to implement a similar locking mechanism to support a single execution of the startup process. Static Lazy<T> initializers remove the need to manage a lock. It ensures the initializer executes on a single thread while blocking all other concurrent threads. 
  1. private static readonly Lazy<IServiceProvider> _serviceProvider;  
The nature of Serverless Function calls for configuration settings to be accessed from cloud-managed resources. If you're familiar with Web APIs, you may be looking for web.config. Azure Function projects include a local.settings.json file that defines a series of name-value pair configuration settings. These setting parameters surface in the application configuration of the Azure Function in the Azure Portal console.
 
If a traditional app.config or web.config file were used to define settings, then updates to the configuration file would require a new software deployment. Deploying an update to production function code requires testing and involves release management processes. Moving settings from configuration files to cloud-managed resources decouple the configuration from code. DevOps teams can manage function configuration settings independently of binary deployments.
 
Azure Functions pull their configuration settings from application and connection string settings managed in the Azure Portal function configuration. The screenshot below shows an ADD_VALUE application setting used by an Azure Function. Production Function settings typically have connection strings to storage accounts and databases that store more complicated configurations than name-value pairs. 
 
 
In a Web API, the configuration settings are read on startup by the first thread to make a request. Any incoming concurrent threads are blocked by the WebHost until the boostrapping of pipelines completes. Serverless Functions don't have the benefit of this scaffolding and are left to manage their own startup sequence and thread locking.
  1.        private const string ADD_VALUE_SETTING = "ADD_VALUE";  
  2.   
  3.        private static readonly Lazy<IServiceProvider> _serviceProvider = new Lazy<IServiceProvider>(() =>  
  4.        {  
  5.            var builder = new ConfigurationBuilder()  
  6.              .AddEnvironmentVariables();  
  7.   
  8.            IConfiguration config = builder.Build();  
  9.   
  10.            IServiceCollection servCol = new ServiceCollection();  
  11.              
  12.            servCol.AddOptions();  
  13.   
  14.            servCol.AddTransient<IAdderRepository, AdderRepository>();  
  15.   
  16.            servCol.Configure<AdderConfig>(addConfig =>  
  17.               addConfig.ValueToAdd = config.GetValue<int>(ADD_VALUE_SETTING)  
  18.            );  
  19.            return servCol.BuildServiceProvider();  
  20.   
  21.        }, true);  
A static Lazy<T> initializer lets a single thread execute while blocking the concurrent threads. It returns an IServiceProvider that is central to the dependency injection framework. It's a registry of configuration data, interfaces, and interface instances. This initializer pulls its configuration settings from environment variables. The same name-value application settings are associated with an Azure Function surface as environment variables. ADD_VALUE_SETTING is read and applied to the AdderConfig.ValueToAdd property.
 
For the sake of simplicity, this sample uses an IAdderRepository interface and AdderRepository implementation. 
  1. public interface IAdderRepository  
  2. {  
  3.     AdderResponse Add(AdderRequest request);  
  4. }  
The implementation of the IAdderRepository interface uses an IOptions<AdderConfig> parameter on the constructor. This carries the ADD_VALUE_SETTING from the Lazy<T> initializer to the AdderRepository implementation. It simply adds the Number property value of the AdderRequest to the ADD_VALUE_SETTING value and returns the result in the Sum property of the AdderResponse. 
  1. public class AdderRepository : IAdderRepository  
  2. {  
  3.     private readonly AdderConfig _adderConfig = null;  
  4.   
  5.     public AdderRepository(IOptions<AdderConfig> adderConfig)  
  6.     {  
  7.         _adderConfig = adderConfig.Value;  
  8.     }  
  9.     
  10.     public AdderResponse Add(AdderRequest request)  
  11.     {  
  12.         AdderResponse resp = new AdderResponse();  
  13.   
  14.         resp.Sum = request.Number + _adderConfig.ValueToAdd;  
  15.   
  16.         return resp;  
  17.     }  
  18. }  
Think of this as a substitute for a production interface and implementation. This could be responsible for querying a database, posting to a queue, or processing business logic. It could also be an existing interface and class from a Web API solution that needs to be ported to a serverless function.
 
The following code is an update of the default HTTP Trigger Azure Function code. It reads a value from the query string, obtains an instance of the IAdderRepository from the Value of the Lazy<T> initializer, and invokes the Add method. The AdderResponse is returned to the caller.
  1. [FunctionName("AdderFunction")]  
  2. public static async Task<IActionResult> Run(  
  3.     [HttpTrigger(AuthorizationLevel.Function, "get""post", Route = null)] HttpRequest req,  
  4.     ILogger log)  
  5. {  
  6.     log.LogInformation("C# HTTP trigger function processed a request.");  
  7.   
  8.     string name = req.Query["val"];  
  9.   
  10.     int sourceVal;  
  11.   
  12.     if (!int.TryParse(name, out sourceVal))  
  13.     {  
  14.         return new BadRequestObjectResult("val not provided or is not an integer");  
  15.     }  
  16.   
  17.     IAdderRepository adderRep = _serviceProvider.Value.GetRequiredService<IAdderRepository>();  
  18.   
  19.     AdderRequest adderReq = new AdderRequest { Number = sourceVal };  
  20.   
  21.     AdderResponse resp = adderRep.Add(adderReq);  
  22.   
  23.     return new OkObjectResult(resp);  
  24. }  
 After deploying the Azure Function, it can be tested from the console.
 
Azure Function Console 
The query string parameter, Val is set to 20 and passed in an HTTP GET request. The function adds that to the ADD_VALUE_SETTING and returns a sum of 30. The request was processed by the Azure Function configured to respond to an HTTP Trigger. The Lazy<T> initializer reads the configuration value and sets up an instance of the IAdderRespository. The Lazy<T> initializer was invoked by the function and returned the IServiceProvider instance in the Value property. The AdderRepository instance was returned in the GetService<T> call and used to process the request.
 

Amazon Web Service Lambdas

 
This technique is not limited to Azure Functions; the same approach applies to AWS Lambdas.
  1. public AdderResponse FunctionHandler(AdderRequest input, ILambdaContext context)  
  2. {  
  3.     var adderRep = _serviceProvider.Value.GetRequiredService<IAdderRepository>();  
  4.   
  5.     AdderResponse resp = adderRep.Add(input);  
  6.   
  7.     return resp;  
  8. }  
The AWS Toolkit for Visual Studio has project templates to create Lambdas using .NET Standard.  If you need to port your Web API to a serverless architecture in AWS, then take a look at AWS Serverless Functions. They are the AWS equivalent to Azure Function Web Triggers.
 

Summary

 
As more organizations move to a serverless architecture, there's a growing need to port the code from legacy Web API projects to Azure Functions and AWS Lambdas. Using a static Lazy<T> initializer to return an IServiceProvider instance with configuration settings, interfaces, and implementation classes, the serverless function can take advantage of the .NET dependency injection framwork. This approach provides a path forward to migrate a business logic from a Web API to serveless functions. 

The sample code included in this article contains all the sample code referenced above and has been tested in both Azure and AWS.