Dependency Injection in Azure Functions: A Comprehensive Guide
All articles from this blog can ONLY be redistributed on an Attribution-NonCommercial-NoDerivs basis. Please credit the source, thank you.
Twitter:@kelvinshen
Blog:Kelvin Shen's Blog
Dependency Injection in Azure Functions: A Comprehensive Guide
Azure Functions provide a serverless compute service that enables you to run small pieces of code without the need to manage the underlying infrastructure. This post explores the dependency injection (DI) pattern within Azure Functions, enhancing maintainability and testability.
We will focus on a simple use case: sending an email using Azure Functions, and explain the DI practices involved in this scenario.
The Example: Sending Emails
Without Dependency Injection
Suppose we want to send emails from our Azure Function. Without dependency injection, you might instantiate your email service directly within the function:
public class SendEmailWithoutDI
{
[FunctionName("SendEmailWithoutDI")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
var emailService = new EmailService();
emailService.Send("example@example.com", "Hello World!");
return new OkResult();
}
}
This tightly couples your function to a specific implementation of the email service, making it harder to test or change the implementation later.
With Dependency Injection
By utilizing dependency injection, we can decouple our function from the specific implementation of the email service. Let’s see how we can achieve this.
IEmailService.cs (Interface Definition)
public interface IEmailService
{
void Send(string to, string body);
}
EmailService.cs (Implementation)
public class EmailService : IEmailService
{
public void Send(string to, string body)
{
// Implementation here
}
}
SendEmail.cs (Azure Function)
public class SendEmail
{
private readonly IEmailService _emailService;
public SendEmail(IEmailService emailService)
{
_emailService = emailService;
}
[FunctionName("SendEmail")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
_emailService.Send("example@example.com", "Hello World!");
return new OkResult();
}
}
Dependency Injection in Azure Functions
Registration with IServiceCollection
Azure Functions use IServiceCollection
as their DI container. You can register your dependencies in the Startup class:
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IEmailService, EmailService>();
}
}
Here we registered EmailService
as a singleton, meaning only one instance will be created and reused across function invocations.
Registration with IServiceCollection: Understanding the Methods and Their Scenarios
The IServiceCollection
interface in Azure Functions provides several methods to register services with different lifetimes. Understanding these methods and when to use them can greatly influence the behavior of your application. Let’s explore each method and the scenarios where they are most suitable:
1. AddSingleton()
AddSingleton
registers a service with a singleton lifetime, meaning the same instance is used across all requests.
services.AddSingleton<IEmailService, EmailService>();
Scenario: Use this method when you want to ensure that only one instance of a service exists within the application. This is particularly useful for services that manage shared resources, such as a connection pool or configuration management.
2. AddScoped()
AddScoped
registers a service that is created once per client request.
services.AddScoped<IOrderService, OrderService>();
Scenario: This method is ideal for services that need to maintain state across a single client request but not across different client requests. For instance, you might use this for a shopping cart service that tracks items for an individual user during a browsing session.
3. AddTransient()
The AddTransient
method is utilized when you need a fresh instance of a service every time it’s requested. This can be particularly useful for services that carry out specific tasks without retaining any state between calls. Let’s examine a more concrete example.
services.AddTransient<IPasswordHasher, PasswordHasher>();
Scenario: Imagine you have a service responsible for hashing passwords within an authentication system, the PasswordHasher
. Since the service doesn’t need to remember anything between calls and you want to ensure that no sensitive information is unintentionally retained, you register it with AddTransient
.
This way, every time the IPasswordHasher
service is requested, a new instance of PasswordHasher
is created. Each operation is entirely independent of others, and there is no risk of information leakage between different parts of the application or different user requests.
By using AddTransient
for such a service, you’re ensuring that the service behaves in a stateless manner, which is typically desired for operations that deal with sensitive or temporary information.
Resolving Instances via Constructor
Azure Functions can resolve instances via the constructor. The registered services are automatically injected into the constructor as parameters, as we saw in SendEmail.cs
.
Resolving Instances via Constructor: A Chain Reaction
In Azure Functions, resolving instances via the constructor sets off a chain reaction, where the DI container takes care of all necessary instance resolutions. This cascading resolution process means that dependencies of dependencies are automatically taken care of, no matter how deep the chain goes.
Let’s expand our example to illustrate this. Suppose our EmailService
depends on an interface called ICrmClient
, which is implemented by a class called CrmClient
. Here’s how we can represent this chain of dependencies:
ICrmClient.cs (Interface Definition)
public interface ICrmClient
{
void Connect();
}
CrmClient.cs (Implementation)
public class CrmClient : ICrmClient
{
public void Connect()
{
// Implementation here
}
}
EmailService.cs (Updated Implementation)
public class EmailService : IEmailService
{
private readonly ICrmClient _crmClient;
public EmailService(ICrmClient crmClient)
{
_crmClient = crmClient;
}
public void Send(string to, string body)
{
_crmClient.Connect();
// Other implementation here
}
}
SendEmail.cs (Azure Function)
public class SendEmail
{
private readonly IEmailService _emailService;
public SendEmail(IEmailService emailService)
{
_emailService = emailService;
}
[FunctionName("SendEmail")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
_emailService.Send("example@example.com", "Hello World!");
return new OkResult();
}
}
In this example, when SendEmail
is instantiated, the DI container first creates an instance of CrmClient
, then uses that to create an instance of EmailService
, and finally uses that to create an instance of SendEmail
. This is all handled automatically, without you having to manually create any of these objects.
This chain reaction of resolution ensures that the dependencies are properly constructed with their own dependencies, providing a smooth and streamlined development experience, and ensuring that each component is provided with the necessary objects to perform its function.
Manual DI Instance Resolution using IServiceProvider
In some cases, you might need to resolve an instance manually using IServiceProvider
. Here’s an example:
public class MyFunction
{
private readonly IServiceProvider _serviceProvider;
public MyFunction(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
[FunctionName("MyFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
var emailService = _serviceProvider.GetService<IEmailService>();
// ...
}
}
Special Case: ILogger Resolution
ILogger
is a special case in Azure Functions. You don’t need to register it; you simply add a parameter of type ILogger
, and the runtime will inject the appropriate logger instance.
Conclusion
Dependency injection in Azure Functions enables a clean separation of concerns, enhances testability, and increases maintainability. By implementing DI in your Azure Functions, you can create more robust and flexible serverless applications. Whether through constructor injection, manual resolution, or the special handling of logging, DI provides powerful tools for developing sophisticated serverless solutions.