Comment organiser votre Startup.cs avec les méthodes d’extension ?
Comment organiser votre Startup.cs avec les méthodes d’extension ?
Si vous êtes un·e développeur·se Asp.Net core, j’imagine que vous êtes familier·ère avec le cauchemar du fichier Startup.cs
.
À mesure que votre projet grandit, ce fichier passe très rapidement à une centaine de lignes vu qu’il peut contenir, entre autres, des configurations, des déclarations de middleware, des définitions d’injection de dépendance ou le paramétrage de l’authentification 🤦.
Si toute la configuration de notre application, notamment la partie injection de dépendance, se trouve dans la classe Startup
c’est un fichier qui va être édité particulièrement souvent ce qui peut être source de conflit lors de merge de branche.
Bien organiser son Startup.cs
est un bon réflexe à adopter, débutants comme expérimentés, pour montrer que vous faites attention à la qualité de votre code.
Dans cet article on va apprendre comment utiliser “les méthodes d’extension” pour bien structurer nos projets Asp.net core et éviter d’avoir un fichier Startup.cs
illisible dans lequel on scrolle à l’infini, le tout dans une logique de “Clean Code”.
Les méthodes d’extension à la rescousse
Les méthodes d’extension (introduites avec C# 3.0) vous permettent d’ajouter des méthodes à des types pré-existants sans créer des types dérivés ou modifier le type d’origine.
En exposant des méthodes d’extension utilisables dans la classe Startup, l’usage des méthodes d’extension est dans la lignée dans ce que fait le framework .net core.
Ex: AddAuthentication
, AddControllers
ou également UseAuthorization
, UseRouting
, etc.
Pour vous donner une idée de leur fonctionnement, on va faire un petit exemple :
On va étendre la class String
avec une nouvelle méthode nommée CountChar(char c) qui retourne le nombre d’occurrence d’un caractère dans un String.
On aura besoin de :
- Une classe statique dans laquelle on va écrire notre méthode d’extension
- Une méthode statique avec une signature un peu spéciale en utilisant le mot clé this avant le type auquel on va ajouter la méthode.
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int CountChar(this String str,Char character)
{
return str.Count(x=> x == character);
}
}
}
Pour utiliser cette méthode dans notre code, il suffit d’importer le namespace ExtensionMethods
dans notre fichier, et voilà !
La magie d’IntelliSense se charge du reste ✨✨. À partir de maintenant, on aura une suggestion avec le nom de notre méthode sur tous les objets de type String.
Vous pouvez voir plus d’exemples sur les méthodes d’extension dans la doc officielle ici.
Place à l’action
Maintenant que l’on connaît le principe, on peut attaquer notre problématique initiale : organiser notre fichier Startup.cs
.
Mais comment on va faire ça ?
On va prendre comme exemple le code d’une configuration de Swagger que l’on trouve dans la majorité des APIs ASP.net core et le refactorer.
Si on analyse un peu le code à ajouter (documentation) on remarque qu’on est en train de manipuler deux objets :
- service qui est une instance de
IServiceCollection
qui sert à configurer le conteneur IoC - app qui est une instance de
IApplicationBuilder
qui sert à configurer l’application et ses middlewares
Donc on va ajouter deux nouvelles méthodes à IServiceCollection
et IApplicationBuilder
méthodes en utilisant les extensions
Comme l’exemple ci-dessus on aura besoin :
- D’une classe statique dans laquelle on va écrire notre méthode d’extension ;
- D’une ou plusieurs méthodes statiques avec une signature un peu spéciale en utilisant le mot clé this avant le type dans lequel on va ajouter la méthode.
namespace ExtensionMethods
{
public static class SwaggerConfigurationExtension
{
public static void AddSwaggerConfig(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
}
public static void UseCustomSwaggerConfig(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
}
Dans le code ci-dessous, nous avons créé deux méthodes d’extensions :
AddSwaggerConfig()
ajoutée à IServiceCollection pour faire la configuration du générateur swagger.UseCustomSwaggerConfig()
ajoutée àIApplicationBuilder
pour ajouter les middlewares swagger générateur et UI.
GIF Animé
Refactoring du Startup.cs
On aura un fichier startup qui ressemble à ça :
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerConfig();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCustomSwaggerConfig();
}
}
Organisation et découpage
Dans cet article, on a vu un exemple de découpage technique dans lequel on a regroupé toutes les configs liées à Swagger dans une seule classe.
Dans le contexte d’un gros projet, il est recommandé d’éviter un découpage technique et aller plutôt vers une approche fonctionnelle les regroupant par “Feature”.
Regrouper sous des méthodes d’extensions techniques semble au premier abord une bonne idée, mais d’expérience on se rend compte rapidement que nous avons juste déporté le problème.
En effet, si on prend par exemple une méthode AddRepositories
qui contient I’enregistrement dans Ie conteneur IOC de l’ensemble de nos repositories, au fur et à mesure que le projet grossit, le nombre de repositories grossit également et le nombre de lignes de cette classe explose pour en revenir à Ia problématique initiale du Startup.
Un découpage basé sur les Bounded Context dans une approche DDD (Domain Driven Design) constitue une alternative intéressante à un découpage technique, vu que celle-ci nous permet d’avoir un découpage fonctionnel proche métier, et ça réduira aussi les conflits dans les phases de développement car 2 développements de 2 fonctionnalités différentes se feront sur 2 bases de code cloisonnées.
Comme ça on peut avoir une organisation modulaire et des projets réutilisables voir packageables autour de chaque bounded context.
Un petit exemple pour illustrer cette approche :
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCatalog();
services.AddOrderFlow();
...
}
}
AddCatalog()
: Extension globale pour ajouter toutes les extensions du Catalogue
namespace WebApp.Catalog.Extensions
{
public static class CatalogExtensions
{
public static void AddCatalog(this IServiceCollection services)
{
services.AddProducts();
services.AddPrices();
}
}
}
AddProducts()
: Extension globale pour ajouter toutes les extensions liées aux produits
namespace WebApp.Catalog.Extensions
{
public static class ProductsExtensions
{
public static void AddProducts(this IServiceCollection services)
{
services.AddProductsRepositories();
services.AddProductsServices();
....
}
}
}
AddProductsRepositories()
: Extension pour ajouter la couche repository liée aux produitsAddProductsServices()
: Extension pour ajouter la couche Services liée aux produits
J’espère que cet article vous a apporté un plus🤜🤛
Avant de finir, je vous invite à méditer cette citation de Martin Fowler, l’un des pionniers du mouvement Clean Code :
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler