Dependency Injection in Azure Functions: A Comprehensive Guide

2023-05-04

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.

Leave a Reply

comments powered by Disqus


Table of Content