Update: Les Azure Functions supportent désormais nativement l’injection de dépendances, grâce à un mécanisme inspiré du Startup de .Net Core. Cet article reste néanmoins valide pour les autres cas d’utilisation. /Update

TL;DR La philosophie fondamentalement statique et procédurale des Azure Functions fait qu’il peut être contre-intuitif d’utiliser des concepts avancés de la programmation objet, tels que l’injection de dépendances.

Cependant plusieurs cas d’utilisation peuvent justifier le recours à ces concepts, pour simplifier l’écriture, les tests et la maintenance.

Heureusement, l’arrivée du support de .Net Core 2.x dans le nouveau runtime des Azure Functions (C#) rend possible l’utilisation des outils du le framework – et donc en particulier l’injection de dépendances.

Uniquement pour les Functions ?

Les outils en question sont dans le package NuGet Microsoft.Extensions.DependencyInjection. Il est fourni par défaut dans le SDK 2.x, mais vous devrez peut-être l’ajouter manuellement.

Dans cet article, je prends pour exemple les Azure Functions, dont les nombreuses fonctionnalités natives peuvent être complétées par tout l’arsenal du framework.

Mais le code ci-après fonctionne également sur toutes les plateformes compatibles .Net standard. On pourrait donc imaginer s’en servir pour une application console, un device IoT, etc.

Configurer avec ServiceCollection

La première étape est d’instancer un objet ServiceCollection qui va nous permettre de configurer l’injection – exactement comme on le ferait dans une application ASP.NET Core classique.

var servicesCollection = new ServicesCollection();

Cet objet ressemble étrangement à celui qui est passé en paramètre à la méthode ConfigureServices(IServiceCollection) du Startup.cs ASP.NET Core. Ce n’est pas un hasard 😉.

Une fois cet objet instancié, vous pouvez utiliser exactement les mêmes méthodes AddSingleton, AddScoped, AddTransient, etc. que dans ASP.NET Core, pour configurer la résolution de vos dépendances en fonction de la durée de vie souhaitée.

Imaginons par exemple une interface ICustomerRepository et l’implémentation correspondante CustomerRepository. Voici comment elle pourrait être configurée dans ServiceCollection :

servicesCollection.AddSingleton<ICustomerRepository, CustomerRepository>();

Instancier avec ServiceProvider

Imaginons maintenant un BillingService qui a besoin d’une instance de ICustomerRepository. Pour pouvoir utiliser cette instance, il faut la déclarer dans le constructeur :

public class BillingService
{
    public BillingService(ICustomerRepository customerRepository) 
    {
        this.customerRepository = customerRepository;
    }
}

L’injection de dépendances nous évite d’avoir à instancier manuellement l’implémentation de ICustomerRepository à utiliser. C’est le moteur d’injection qui va nous la fournir via le constructeur.

Comment ça 🤔 ?

La ServiceCollection créée plus haut fournit une méthode BuildServiceProvider qui retourne un provider capable de nous instancier les objets et leurs dépendances de manière automatique.

var services = serviceCollection.BuildServiceProvider();
var billingService = services.GetRequiredService<BillingService>();

L’instance billingService est créée dynamiquement et son champ customerRepository est valorisé avec une instance de CustomerRepository via le constructeur.

Il faut également au préalable déclarer notre BillingService dans la ServiceCollection :

serviceCollection.AddSingleton<BillingService>();

Enrichissement

Bon : ça commence à faire beaucoup de code pour instancer seulement deux objets. Mais mon exemple reste relativement trivial.

Imaginez que votre BillingService ait aussi besoin d’un IOrderRepository en plus de ICustomerRepository. Imaginez en plus que cet IOrderRepository ait lui aussi besoin de ICustomerRepository.

L’injection de dépendances vous évite de devoir réfléchir à l’ordre et à la manière dont sont instanciés tous ces objets.

Vous pouvez en plus injecter des composants du framework lui-même.

Le gain est réellement significatif quand on a une hiérarchie de composants qui ont besoin les uns des autres, par exemple avec un découpage en couches métier (contrôleur, service, entity) et qui ont également besoin de services tiers.

Un bon vieux code lasagne comme on les aime 🙂🍕.

Code final

Si on met tout les morceaux ensemble, on obtient ceci :

private static IServiceProvider ConfigureServices()
{
    var serviceCollection = new ServiceCollection();

    serviceCollection.AddSingleton<BillingService>();
    serviceCollection.AddSingleton<ICustomerRepository, CustomerRepository>();

    return serviceCollection.BuildServiceProvider();
}

Dans notre Azure Function, à l’endroit où on a besoin d’une instance de BillingService :

private static readonly IServiceProvider services = ConfigureServices();
private static readonly BillingService billingService = services.GetRequiredService<BillingService>();

Pensez bien à déclarer ces variables static afin de bénéficier de l’effet de cache des Azure Functions.

En effet, une fois son exécution terminée, la classe contenant votre code reste en mémoire au minimum cinq minutes avant d’être déchargée. Si, dans cet intervalle, votre fonction est à nouveau appelée, l’instance billingService déclarée statique sera toujours présente en mémoire et n’aura pas besoin d’être à nouveau initialisée.

Si vous injectez des ressources coûteuses (comme un client http par exemple), vous éviterez un démarrage à froid qui ralentit considérablement les temps de réponse des fonctions.

Conclusion

Ce mécanisme d’injection de dépendances, initialement présent dans ASP.NET core, fonctionne désormais sur toutes les plateformes compatibles .NET standard, y compris les Azure Functions V2.