Top 25 questions d'entretien ASP.NET Core : middleware, DI et minimal APIs

Les questions d'entretien ASP.NET Core les plus courantes sur le pipeline de middleware, les durées de vie de l'injection de dépendances et les minimal APIs. Couvre les nouveautés de .NET 9 et .NET 10 avec des exemples de code.

Questions d'entretien ASP.NET Core sur le middleware, l'injection de dépendances et les minimal APIs

Les questions d'entretien ASP.NET Core sur le middleware, l'injection de dépendances et les minimal APIs restent parmi les sujets les plus fréquents des entretiens techniques .NET. Que le poste cible le développement backend ou des positions full-stack .NET, les recruteurs sondent systématiquement la compréhension du pipeline de requêtes, des durées de vie des services et du modèle d'API léger introduit dans .NET 6 et affiné jusqu'à .NET 10.

Ce que les recruteurs testent vraiment

L'ordre du pipeline de middleware, les incompatibilités de durée de vie en DI (le problème de la dépendance captive) et la capacité à expliquer les compromis entre minimal APIs et contrôleurs. Ces trois domaines représentent la majorité des questions d'entretien ASP.NET Core aux niveaux intermédiaire et senior.

Questions sur le pipeline de middleware ASP.NET Core

1. Qu'est-ce qu'un middleware dans ASP.NET Core ?

Un middleware est un composant assemblé dans le pipeline de l'application pour traiter les requêtes et les réponses. Chaque composant de middleware choisit de transmettre la requête au composant suivant ou de court-circuiter le pipeline. Le middleware s'exécute dans l'ordre de son enregistrement dans Program.cs, et la réponse remonte à travers les mêmes composants dans l'ordre inverse.

2. Comment fonctionne l'ordre d'exécution du middleware ?

L'ordre des appels app.Use*() dans Program.cs définit la séquence d'exécution exacte. Une requête circule de haut en bas à travers chaque middleware, et la réponse remonte de bas en haut. C'est pourquoi UseAuthentication() doit apparaître avant UseAuthorization(), et UseRouting() avant UseEndpoints().

Program.cs — L'ordre du middleware comptecsharp
var app = builder.Build();

app.UseExceptionHandler("/error");   // 1. Outermost: catches all exceptions
app.UseHsts();                        // 2. HTTP Strict Transport Security
app.UseHttpsRedirection();            // 3. Redirect HTTP to HTTPS
app.UseStaticFiles();                 // 4. Serve static files (short-circuits if found)
app.UseRouting();                     // 5. Match request to endpoint
app.UseAuthentication();              // 6. Identify the user
app.UseAuthorization();               // 7. Check permissions
app.MapControllers();                 // 8. Execute the matched endpoint

app.Run();

Placer UseStaticFiles() avant l'authentification évite des vérifications d'autorisation inutiles sur les requêtes CSS, JS et images. C'est une question de suivi fréquente en entretien.

3. Comment écrire un middleware personnalisé ?

Un middleware personnalisé peut être implémenté sous forme de classe avec une méthode InvokeAsync ou en ligne avec app.Use(). L'approche par classe est préférée en production car elle prend en charge l'injection de dépendances via les paramètres du constructeur.

RequestTimingMiddleware.cscsharp
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        
        await _next(context);  // Call the next middleware
        
        stopwatch.Stop();
        _logger.LogInformation(
            "Request {Method} {Path} completed in {Elapsed}ms",
            context.Request.Method,
            context.Request.Path,
            stopwatch.ElapsedMilliseconds);
    }
}

// Registration in Program.cs
app.UseMiddleware<RequestTimingMiddleware>();

À noter : les classes de middleware sont instanciées une seule fois (durée de vie singleton) tandis que InvokeAsync est appelée à chaque requête. Cette distinction est importante pour la sécurité des threads.

4. Quelle est la différence entre app.Use(), app.Run() et app.Map() ?

app.Use() ajoute un middleware qui appelle le délégué suivant. app.Run() est un middleware terminal qui n'appelle pas le suivant, ce qui termine le pipeline. app.Map() crée une branche du pipeline en fonction du chemin de la requête. Une erreur courante consiste à placer app.Run() trop tôt, ce qui court-circuite tout le middleware en aval.

5. Comment fonctionne le court-circuitage du middleware ?

Tout middleware peut court-circuiter le pipeline en n'appelant pas next(). C'est ainsi que fonctionne UseStaticFiles() : si un fichier statique correspond au chemin de la requête, il le retourne directement sans invoquer le middleware d'authentification, d'autorisation ou de routage. Le middleware de limitation de débit utilise aussi le court-circuitage pour retourner des réponses 429 avant d'atteindre l'endpoint.

6. Qu'est-ce qui a changé pour le middleware dans .NET 9 ?

.NET 9 a étendu la prise en charge de l'injection de dépendances par clé au middleware. L'attribut [FromKeyedServices] fonctionne désormais à la fois dans le constructeur du middleware (pour les services singleton/transient) et dans la méthode InvokeAsync (pour toutes les durées de vie, y compris scoped). Cela élimine le besoin de contournements de type service locator lorsqu'un middleware a besoin de différentes implémentations de la même interface.

Questions d'entretien sur l'injection de dépendances

7. Quelles sont les trois durées de vie de service en DI ?

ASP.NET Core fournit trois durées de vie : Transient (nouvelle instance à chaque fois), Scoped (une instance par requête HTTP) et Singleton (une instance pour toute la durée de vie de l'application). Ce choix affecte directement l'utilisation de la mémoire, la sécurité des threads et le partage d'état entre composants.

Le problème de la dépendance captive

Injecter un service scoped dans un singleton crée une dépendance captive : le service scoped vit aussi longtemps que le singleton et partage son état entre les requêtes. Activez ValidateScopes en développement pour détecter cela au démarrage.

8. Expliquer le problème de la dépendance captive avec un exemple

Lorsqu'un service singleton reçoit une dépendance scoped via son constructeur, cette instance scoped n'est jamais libérée entre les requêtes. Elle devient « captive » dans la durée de vie du singleton. Cela conduit à un partage d'état entre les requêtes HTTP, ce qui peut provoquer des fuites de données et des conditions de concurrence.

Program.cs — Captive dependency detectioncsharp
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddSingleton<ICacheService, CacheService>(); // CacheService takes IUserRepository

// This throws at startup in Development when ValidateScopes is enabled:
builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;    // Catches captive dependencies
    options.ValidateOnBuild = true;   // Validates all registrations at startup
});

La solution : injecter IServiceScopeFactory dans le singleton et créer un scope manuellement lorsque le service scoped est nécessaire.

9. Que sont les services par clé (keyed services) et quand les utiliser ?

Les services par clé, introduits dans .NET 8, permettent d'enregistrer plusieurs implémentations de la même interface avec des clés différentes. Cela élimine les patterns de factory personnalisés et la logique de résolution conditionnelle complexe.

Program.cs — Registering keyed servicescsharp
builder.Services.AddKeyedSingleton<INotificationService, EmailNotification>("email");
builder.Services.AddKeyedSingleton<INotificationService, SmsNotification>("sms");
builder.Services.AddKeyedSingleton<INotificationService, PushNotification>("push");

// Resolving in a controller or service
public class OrderService
{
    public OrderService(
        [FromKeyedServices("email")] INotificationService emailService,
        [FromKeyedServices("sms")] INotificationService smsService)
    {
        // Each parameter gets its specific implementation
    }
}

Avant les services par clé, cela nécessitait soit une classe factory, soit des enregistrements nommés avec des conteneurs tiers, soit une résolution manuelle depuis IServiceProvider.

10. Faut-il encore utiliser des conteneurs DI tiers avec ASP.NET Core ?

Le conteneur intégré couvre la plupart des scénarios à partir de .NET 9/10 : injection par constructeur, services par clé, génériques ouverts et patterns de décorateur (via enregistrement manuel). Les conteneurs tiers comme Autofac ou Lamar restent utiles pour les scénarios avancés tels que l'injection par propriété, l'interception/AOP, l'enregistrement par module ou le scan par convention sur de grands assemblages. Pour la plupart des applications, le conteneur intégré suffit.

11. Comment fonctionne la DI dans les minimal APIs par rapport aux contrôleurs ?

Les contrôleurs reçoivent leurs dépendances par injection de constructeur. Les endpoints de minimal API les reçoivent comme paramètres de méthode, résolus automatiquement depuis le conteneur DI. Des types spéciaux comme HttpContext, CancellationToken et ClaimsPrincipal sont aussi liés automatiquement sans enregistrement explicite.

12. Qu'est-ce que le pattern Options ?

Le pattern Options lie des sections de configuration à des classes fortement typées. Il sépare la configuration de la logique de service et prend en charge la validation, les options nommées et le rechargement à l'exécution via IOptionsMonitor<T>. Injecter directement IConfiguration dans les services est un anti-pattern car cela couple étroitement les services à la structure de configuration.

Prêt à réussir tes entretiens .NET ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Questions d'entretien sur les minimal APIs

13. Que sont les minimal APIs et en quoi diffèrent-elles des contrôleurs ?

Les minimal APIs, introduites dans .NET 6, définissent les endpoints HTTP directement dans Program.cs à l'aide de app.MapGet(), app.MapPost() et autres méthodes similaires. Elles éliminent le besoin de classes de contrôleur, d'attributs de model binding et de filtres d'action. Les contrôleurs offrent une prise en charge intégrée des filtres, de la validation de modèle et de la négociation de contenu. Les minimal APIs sont plus légères mais nécessitent une configuration manuelle pour les préoccupations transversales.

14. Quand choisir les minimal APIs plutôt que les contrôleurs ?

Les minimal APIs conviennent aux microservices, aux API légères et aux scénarios où les performances de démarrage comptent. Les contrôleurs conviennent aux grandes applications avec des pipelines de filtrage complexes, un model binding étendu, ou des équipes habituées aux conventions MVC. Les deux peuvent coexister dans la même application.

15. Comment structurer les minimal APIs dans un vrai projet ?

Évitez de placer tous les endpoints dans Program.cs. Utilisez des méthodes d'extension pour organiser les endpoints par fonctionnalité ou par domaine, en gardant les gestionnaires de route minces et en déléguant la logique métier aux services.

Features/Products/ProductEndpoints.cscsharp
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products")
            .RequireAuthorization();

        group.MapGet("/", GetAll);
        group.MapGet("/{id:int}", GetById);
        group.MapPost("/", Create);
    }

    private static async Task<IResult> GetAll(
        IProductService productService,    // Auto-resolved from DI
        CancellationToken cancellationToken)
    {
        var products = await productService.GetAllAsync(cancellationToken);
        return TypedResults.Ok(products);
    }

    private static async Task<IResult> GetById(
        int id,
        IProductService productService)
    {
        var product = await productService.GetByIdAsync(id);
        return product is null
            ? TypedResults.NotFound()
            : TypedResults.Ok(product);
    }

    private static async Task<IResult> Create(
        CreateProductRequest request,
        IProductService productService)
    {
        var product = await productService.CreateAsync(request);
        return TypedResults.Created($"/api/products/{product.Id}", product);
    }
}

// Program.cs — Clean registration
app.MapProductEndpoints();

Ce pattern passe bien à l'échelle : chaque dossier de fonctionnalité contient ses endpoints, ses DTO et ses interfaces de service.

16. Comment fonctionne la validation dans les minimal APIs ?

.NET 10 a introduit la validation intégrée pour les minimal APIs. Avant .NET 10, la validation nécessitait une implémentation manuelle ou des bibliothèques tierces comme FluentValidation. Avec .NET 10, les annotations de données sur les types de paramètres sont validées automatiquement, et les requêtes invalides retournent une réponse 400 avec des problem details.

17. Qu'est-ce que TypedResults et pourquoi l'utiliser ?

TypedResults (introduit dans .NET 7) offre une sécurité de type à la compilation pour les réponses d'API et active la génération automatique de documentation OpenAPI. Contrairement à Results, qui retourne IResult, les méthodes de TypedResults retournent des types spécifiques comme Ok<T> ou NotFound, ce qui permet au framework de générer des schémas de réponse précis.

18. Comment gérer les Server-Sent Events dans les minimal APIs ?

.NET 10 a ajouté TypedResults.ServerSentEvents() pour les réponses en streaming. Cela élimine l'écriture manuelle dans Response.Body et gère automatiquement la sérialisation JSON, les en-têtes content-type et la gestion de la connexion.

Questions d'entretien avancées

19. Comment le pipeline de requêtes interagit-il avec le routage d'endpoints ?

UseRouting() met en correspondance la requête entrante avec un endpoint mais ne l'exécute pas. Le middleware situé entre UseRouting() et MapControllers()/MapEndpoints() peut inspecter les métadonnées de l'endpoint correspondant (par exemple les attributs d'autorisation) avant l'exécution. C'est ainsi que UseAuthorization() lit les attributs [Authorize] définis sur les contrôleurs ou les endpoints de minimal API.

20. Quelle est la différence entre AddTransient, AddScoped et AddSingleton pour IHttpClientFactory ?

Ne jamais enregistrer HttpClient en singleton : il ignore les changements DNS et épuise les connexions de socket. IHttpClientFactory (via AddHttpClient()) gère correctement les durées de vie de HttpClient : les instances de HttpClient sont transient, mais le pool de HttpMessageHandler sous-jacent est géré avec une durée de vie configurable (2 minutes par défaut). Cela évite l'épuisement des sockets tout en respectant les TTL DNS.

21. Comment implémenter le pattern décorateur avec la DI intégrée ?

Le conteneur intégré ne prend pas en charge nativement les décorateurs, mais cela peut être réalisé manuellement à l'aide de délégués de factory.

csharp
// Decorating IProductService with caching
builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<IProductService>(sp =>
{
    var inner = sp.GetRequiredService<ProductService>();
    var cache = sp.GetRequiredService<IMemoryCache>();
    return new CachedProductService(inner, cache);
});

Pour les applications nécessitant de nombreux décorateurs, un conteneur tiers comme Scrutor fournit une API Decorate<TInterface, TDecorator>() plus propre.

22. En quoi le middleware diffère-t-il des endpoint filters dans les minimal APIs ?

Le middleware s'exécute pour chaque requête du pipeline, quel que soit l'endpoint. Les endpoint filters (introduits dans .NET 7) ne s'exécutent que pour les endpoints correspondants, à l'image des filtres d'action en MVC. Les filtres ont accès au contexte spécifique à l'endpoint et sont enregistrés par route ou par groupe. Utilisez le middleware pour les préoccupations transversales comme le logging et CORS ; utilisez les filtres pour la logique propre à l'endpoint comme la validation ou la transformation.

23. Qu'est-ce que HybridCache dans .NET 9 ?

HybridCache combine la mise en cache en mémoire (L1) et distribuée (L2) derrière une seule API. Il gère automatiquement la protection contre le cache stampede : lorsque plusieurs requêtes concurrentes manquent le cache pour la même clé, une seule exécute la méthode factory pendant que les autres attendent. Cela remplace la coordination manuelle entre IMemoryCache et IDistributedCache, auparavant sujette aux erreurs.

24. Comment tester un middleware de façon isolée ?

Un middleware peut être testé à l'aide de DefaultHttpContext et d'un RequestDelegate personnalisé. Créez une instance du middleware, passez-lui un contexte simulé et vérifiez les propriétés de la réponse.

RequestTimingMiddlewareTests.cscsharp
[Fact]
public async Task Middleware_LogsElapsedTime()
{
    var logger = new FakeLogger<RequestTimingMiddleware>();
    var middleware = new RequestTimingMiddleware(
        next: (ctx) => Task.CompletedTask,  // Terminal delegate
        logger: logger);

    var context = new DefaultHttpContext();
    context.Request.Method = "GET";
    context.Request.Path = "/api/products";

    await middleware.InvokeAsync(context);

    Assert.Single(logger.LogEntries);
    Assert.Contains("completed in", logger.LogEntries[0].Message);
}

Pour les tests d'intégration, WebApplicationFactory<T> fournit un test de pipeline complet incluant l'ordre du middleware.

25. Quels sont les changements OpenAPI dans .NET 9 et .NET 10 ?

.NET 9 a remplacé Swagger/Swashbuckle par la génération intégrée de documents OpenAPI via Microsoft.AspNetCore.OpenApi. .NET 10 est passé à la bibliothèque OpenAPI v2.0 avec prise en charge du YAML, des schémas de types nullable améliorés (utilisant oneOf au lieu de la propriété nullable) et une meilleure intégration des commentaires de documentation XML. Les documents générés sont servis sur /openapi/v1.json par défaut. Scalar ou Swagger UI peuvent être ajoutés comme paquet séparé pour une documentation interactive.

Conseil de préparation

Entraînez-vous à expliquer le pipeline de middleware au tableau : dessinez la requête qui descend à travers chaque middleware et la réponse qui remonte. Les recruteurs d'entreprises comme Microsoft, Accenture et les cabinets de conseil utilisent fréquemment cet exercice pour évaluer la profondeur de la compréhension.

Conclusion

  • L'ordre d'exécution du middleware est déterministe et critique pour la sécurité : gestion des exceptions en premier, fichiers statiques avant l'authentification, routage avant l'autorisation
  • Le problème de la dépendance captive (scoped à l'intérieur d'un singleton) est le bug de DI le plus courant ; activez ValidateScopes et ValidateOnBuild en développement
  • Les services par clé de .NET 8/9 éliminent la plupart des besoins en conteneurs DI tiers ou en patterns de factory personnalisés
  • Les minimal APIs et les contrôleurs coexistent ; choisissez selon la complexité du projet, les conventions de l'équipe et les besoins de filtrage
  • Structurez les minimal APIs à l'aide de méthodes d'extension et de groupes de routes pour garder Program.cs propre
  • .NET 10 ajoute la validation intégrée des minimal APIs, la prise en charge des SSE via TypedResults et une génération OpenAPI améliorée
  • Testez le middleware de façon isolée avec DefaultHttpContext ; utilisez WebApplicationFactory pour les tests d'intégration

Entraînez-vous sur ces questions avec du vrai code sur le simulateur d'entretien .NET de SharpSkill couvrant les modules minimal APIs, développement Web API et clean architecture.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#asp.net core
#interview
#middleware
#dependency injection
#minimal apis
#.net

Partager

Articles similaires