Comment éviter de (re)build le service provider ?
Pré-requis
- .NET 6+
- Avoir des connaissances basiques d’injection de dépendances (DIP, IOC)
Contexte
Il arrive que nous ayons besoin de classes qui ont été configurées dans la DI, lors de la construction de cette même DI.
Dans mon cas, si le run de l’application était en local, pour du dev, je devais exécuter une import de fichier directement. Sinon, l’execution de l’opération devait être délégué via un système de queue sur un cloud provider (implémentation qui était déjà existante).
Il faut donc activer une fonctionnalité (👋 features flags) en fonction de certains paramètres configurés au lancement de notre application.
Solution
Configuration du paramétrage
Utilisons déjà le pattern Options.
Ainsi, si nous avons configuré une classe FeatureFlags avec une propriété IsQueueActivated de type boolean, nous pourrions avoir besoin de resolve cette même classe pour avoir la classe, et ses propriétés, rempli automatiquement !
1
2
3
4
5
public class FeatureFlags
{
public bool IsQueueActivated { get; set; }
}
Le fichier appsettings.json contient :
1
2
3
4
5
6
{
"FeatureFlags":
{
"IsQueueActivated": true
}
}
Pour la configuration de la DI afin de récupérer la valeur de IsQueueActivated :
1
2
IServiceCollections services; // à titre informatif, pour comprendre le type de la variable services
services.AddOptions<FeatureFlags>().BindConfiguration("FeatureFlags");
Définition des 2 classes de traitement
À présent, nous allons définir nos deux classes, la première qui aura la responsabilité de gérer le cas où nous voulons passer par du traitement sans queue :
1
2
3
4
5
6
7
public class HandleLocalImportRequest
{
public async Task ExecuteAsync()
{
// mon implémentation
}
}
Et la seconde, avec le système de queue :
1
2
3
4
5
6
7
public class HandleCloudCancelImportRequest
{
public async Task ExecuteAsync()
{
// mon implémentation
}
}
Je vais aussi définir un contrat pour cacher ces implémentations concrètes. Mes deux classes ci-dessus vont donc aussi implémenter l’interface ci-dessous :
1
2
3
4
public interface IHandleImportRequest
{
public Task ExecuteAsync();
}
Deux classes distinctes qui auront donc chacune une responsabilité différente !
Mise en place d’une factory pour configurer la DI
Je souhaite maintenant pouvoir choisir une des implémentations selon la valeur de IsQueueActivated. Je pourrais donc naïvement effectuer l’opération suivante :
1
2
3
4
5
6
7
8
9
10
11
12
13
var serviceProvider = services.BuildServiceProvider();
var featureFlags = serviceProvider.GetService<IOptions<FeatureFlags>>();
if (featureFlags is not null &&
featureFlags.Value.IsQueueActivated)
{
services.TryAddTransient<IHandleImportRequest, HandleCloudCancelImportRequestEvent>();
}
else
{
services.TryAddTransient<IHandleImportRequest, HandleLocalImportRequest>();
}
Et là, c’est le drame ! En effet, nous avons reconstruit le service provider alors qu’il sera déjà construit plus tard, lors du run de notre application. Ce qui implique que si nous avons définis des services en tant que Singleton, des copies de ces services seront créés, et cela, autant de fois que nous construirons le service provider.
L’alerte d’analyse de code ASP0000 devrait être soulevé dans votre IDE préféré d’ailleurs.
Comment l’éviter, tout en permettant de récupérer notre configuration via la DI ?
En utilisant une factory.
Ce qui fait que la résolution est déléguée au moment où le code exécuté aura une dépendance au contrat IHandleImportRequest dans notre base de code. Ainsi, pas besoin de rebuild le service provider pour récupérer FeatureFlags :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
services.TryAddTransient<IHandleImportRequest>(serviceProvider =>
{
var featureFlags = serviceProvider.GetService<IOptions<FeatureFlags>>();
if (featureFlags is not null &&
featureFlags.Value.IsQueueActivated)
{
return new HandleCloudCancelImportRequestEvent();
}
else
{
return new HandleLocalCancelImportRequestEvent();
}
});
Conclusion
Vous pouvez retrouver sur cette issue GitHub, un échange plus détaillé des impacts lorsque l’on souhaite build le service provider alors que nous sommes en train de le construire.
On peut y lire notamment une intervention de David Fowler.
Concernant la mise en place du concept de features flags, il est aussi possible d’utiliser la librairie proposée par Microsoft. Même si l’exemple est donné dans le contexte ASP.Net Core, la librairie de base peut être utilisé sans en dépendre (cf paquet Nuget).
Essayez de laisser ce monde un peu meilleur qu’il ne l’était quand y êtes venus. (Baden-Powell)