1. Introduction
Dans beaucoup d’applications ASP.Net le pattern MVC (Modèle-vue-contrôleur) est implémenté. Lorsqu’on souhaite développer une API, la notion de contrôleur est souvent utilisée. Pour se détacher de cette approche propre au contrôle d’une IHM, nous vous proposons une approche différente avec l’utilisation de endpoints.
Un endpoint décrit la relation entre une route (URL) et un service web.
2. UseEndpoints
Nous allons parler ici de comment architecturer une API en .Net 5 sans utiliser de contrôleurs.
Depuis .Net Core 3 il est possible d’utiliser les endpoints dans Kestrel.
Pour ce faire, il suffit d’utiliser la méthode UseEndpoints dans le Configure
de votre startup.cs :
app.UseRouting();
app.UseEndpoints(endpoints =>
{
...
});
endpoints
implémente l’interface IEndpointRouteBuilder
et expose des méthodes pour définir des routes directement dans le fichier startup.cs. :
- MapGet
- MapPost
- MapPut
- MapDelete …
Nous pouvons donc indiquer à Kestrel comment il doit interpréter les requêtes reçues pour telle ou telle route, exemple :
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/api/", async (context) => { context.Response.StatusCode = (int)HttpStatusCode.NoContent; });
});
Il est alors possible de créer toute une API de cette manière!
Prenons l’exemple d’une API qui gère des utilisateurs :
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/api/users/", async (context) =>
{
try
{
// Permet de récupérer les services des autres instances injectées
var serviceProvider = context.RequestServices;
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
endpoints.MapGet($"/api/users/{{id}}", async (context) =>
{
try
{
var serviceProvider = context.RequestServices;
// On récupère l'Id depuis la route
var stringValues = context.Request.RouteValues["id"].ToString();
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
endpoints.MapPost("/api/users/", async (context) =>
{
try
{
var serviceProvider = context.RequestServices;
var requestBody = await context.Request.ReadFromJsonAsync<object>();
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
});
Nous pouvons définir ici toutes les routes de notre API et agir en conséquence.
3. UseAPIEndpoints
Afin de mieux organiser le code, nous allons déporter la définition des routes dans une autre classe nommée APIRoutes.cs
public static class APIRoutes
{
private const string _apiName = "api/users/";
internal static IEndpointRouteBuilder UseAPIEndpoints(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet($"/{_apiName}/ ", async (context) =>
{
try
{
// Permet de récupérer les services est autres instances injectées
var serviceProvider = context.RequestServices;
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
endpoints.MapPost($"/{_apiName}/", async (context) =>
{
try
{
var serviceProvider = context.RequestServices;
var requestBody = await context.Request.ReadFromJsonAsync<object>();
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
endpoints.MapGet($"/{_apiName}/{{id}}", async (context) =>
{
try
{
var serviceProvider = context.RequestServices;
// On récupère l'Id depuis la route
var stringValues = context.Request.RouteValues["id"].ToString();
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
});
return endpoints;
}
}
Notre UseEndpoints
(startup.cs) devient donc :
app.UseEndpoints(endpoints =>
{
endpoints.UseAPIEndpoints();
});
4. Factoriser le code
Cela allège déjà le fichier startup.cs mais si on s’arrête là, la classe APIRoutes est déjà conséquente alors que nous n’avons définit que trois endpoints !
Pour chaque endpoint nous allons créer une classe associée dans des fichiers différents .
Chaque classe implémente une méthode statique Delegate
contenant le code qui interprète la requête HTTP :
public class GetUsersDelegate
{
public static RequestDelegate Delegate => async context =>
{
var serviceProvider = context.RequestServices;
var logger = serviceProvider.GetService<ILogger<GetUsersDelegate>>();
try
{
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
logger.LogError(ex.Message);
logger.LogTrace(ex.StackTrace);
await context.KO(ex);
}
};
}
public class PostUserDelegate
{
public static RequestDelegate Delegate => async context =>
{
var serviceProvider = context.RequestServices;
var logger = serviceProvider.GetService<ILogger<PostUserDelegate>>();
try
{
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
logger.LogError(ex.Message);
logger.LogTrace(ex.StackTrace);
await context.KO(ex);
}
};
}
public class GetUserByIdDelegate
{
public static RequestDelegate Delegate => async context =>
{
var serviceProvider = context.RequestServices;
var logger = serviceProvider.GetService<ILogger<GetUserByIdDelegate>>();
try
{
// On récupère l'Id depuis la route
var stringValues = context.Request.RouteValues["id"].ToString();
context.Response.StatusCode = (int)HttpStatusCode.NoContent;
}
catch (Exception ex)
{
logger.LogError(ex.Message);
logger.LogTrace(ex.StackTrace);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync(ex.Message);
}
};
}
La méthode UseEnvironmentEndpoints
de APIRoutes.cs devient donc :
internal static IEndpointRouteBuilder UseEnvironmentEndpoints(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet($"/{_apiName }/ ", GetUsersDelegate.Delegate);
endpoints.MapGet($"/{_apiName }/{{id}}", GetUserByIdDelegate.Delegate);
endpoints.MapPost($"/{_apiName }/", PostUserDelegate.Delegate);
return endpoints;
}
Et voilà !
Rien ne vous empêche ensuite de créer d’autres fichiers de type APIRoutes.cs et de séparer vos APIs par domaine métier.
Voici à présent l’arborescence du projet :
Root
│ Programme.cs
│ Startup.cs
│
└───RequestDelegates
│ └───API
│ │ GetUserByIdDelegate.cs
│ │ GetUsersDelegate.cs
│ │ PostUserDelegate.cs
│ │ ...
│
└───Routes
│ APIRoutes.cs
5. Conclusion
Nous avons vu comment créer une API en se passant de contrôleurs.
Cette forme permet de centraliser la gestion des routes.
Cela permet une compréhension rapide de ce que fait l’API à la simple lecture du fichier APIRoutes.cs
.
Il n’y a plus besoin de se balader dans les différents contrôleurs pour savoir ce que fait l’application.
Libre à vous ensuite de séparer la définition des routes en plusieurs fichiers (par domaine métier par exemple).
On peut reprocher à cette méthode de perdre l’avantage que nous donne le constructeur du contrôleur lorsque plusieurs actions utilisent les mêmes instances de classe (injection de dépendance) cependant si une action ne nécessite pas tout ce qui est fait dans le constructeur (exemple : lecture de configuration), il peut y avoir une légère perte de performances. De plus si la couche HTTP de votre application est bien conçue, elle devrait avoir comme fonction principale de rediriger les requêtes vers les commandes / queries de la couche métier (cf. CQRS Command Query Responsibility Segregation).
Et pour ce qui est de la gestion de la sécurité ou autre filtre des requêtes, ASP.Net 5 nous fournis déjà tout l’attirail nécessaire pour ne pas implémenter ça dans nos requestDelegates : intergiciel (middleware), IAuthorizationHandler.
Liens Externes :
- Vous trouverez l’intégralité du code associé à cet article sur notre Github : https://github.com/c2s-bouygues/blog.c2s.endpoints
- les endpoints MSDN
- Un article de
Oskar Dudycz
sur la manière dont on peut organiser son code de manière efficace (https://event-driven.io/en/how_to_slice_the_codebase_effectively/)