Plateforme de développement de Microsoft qui fournit un runtime (CLR) et un ensemble de bibliothèques pour exécuter des applications écrites en C#, F#, VB.NET, etc.
Aujourd’hui, on parle plutôt de .NET « unifié » (.NET 6, 7, 8…) multiplateforme (Windows, Linux, macOS).
Questions d’entretien .NET
Une sélection de questions/réponses .NET pour préparer tes entretiens. Utilise les onglets pour filtrer par niveau.
• .NET Framework : historique, Windows only, installé via Windows, encore très présent en production mais plutôt pour des applis existantes.
• .NET (Core, 5, 6, 7, 8…) : multiplateforme, open-source, beaucoup plus performant, c’est la base pour tous les nouveaux projets (API, services, Blazor, MAUI…).
Un assembly est l’unité de déploiement et de versionning en .NET (fichier .dll ou .exe) qui contient le code compilé (IL) et les métadonnées nécessaires au CLR.
C’est ce que tu déploies sur le serveur ou dans un conteneur.
• Value type (struct, int, bool, etc.) : stocké généralement sur la stack, la valeur est copiée lors d’une affectation.
• Reference type (class, array, string, etc.) : stocké sur le heap, les variables contiennent une référence vers l’objet.
Conséquence : les modifications sur un reference type via une variable sont visibles via l’autre variable.
Un composant du CLR qui libère automatiquement la mémoire des objets non référencés.
Il fonctionne par générations (Gen0, Gen1, Gen2) pour optimiser les performances : les objets jeunes meurent vite, les objets « survivants » montent en génération.
NuGet est le gestionnaire de packages de l’écosystème .NET.
Il permet :
• d’ajouter des bibliothèques externes (JSON, logging, DI, etc.),
• de gérer les versions et dépendances,
• de partager tes propres packages internes (feed privé, GitHub Packages, Azure Artifacts…).
LINQ (Language Integrated Query) est un ensemble d’extensions qui permet d’écrire des requêtes sur des collections ou des sources de données de manière déclarative (Where, Select, GroupBy…).
Exemple simple sur une liste :
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 10)
.ToList();
// Résultat : 20, 40
• Une class fournit une implémentation (méthodes, propriétés, champs…) et peut contenir de l’état.
• Une interface définit un contrat (méthodes / propriétés) que plusieurs classes peuvent implémenter.
Exemple :
public interface IRepository<T>
{
Task<T?> GetByIdAsync(int id);
}
public class UserRepository : IRepository<User>
{
public Task<User?> GetByIdAsync(int id)
{
// implémentation concrète
}
}
Un record est un type de référence conçu pour représenter des données immuables, avec une égalité par valeur (deux records sont égaux si leurs propriétés sont égales).
Exemple :
public record UserDto(int Id, string Email);
var u1 = new UserDto(1, "a@b.com");
var u2 = new UserDto(1, "a@b.com");
// u1.Equals(u2) == true
On l’utilise beaucoup pour les DTO, messages, events, etc.
Un enum est un type composé d’un ensemble de constantes nommées (par défaut basées sur int).
Exemple :
public enum OrderStatus
{
Pending = 0,
Paid = 1,
Shipped = 2,
Cancelled = 3
}
On s’en sert pour rendre le code plus lisible que des simples entiers.
Ce sont des mots-clés qui simplifient l’écriture du code asynchrone basé sur Task.
Ils permettent de ne pas bloquer le thread (important pour le thread UI ou les requêtes HTTP) tout en gardant un code lisible.
Exemple :
public async Task<WeatherForecast> GetAsync(int id)
{
var entity = await _db.Forecasts.FindAsync(id);
if (entity is null) throw new NotFoundException();
return entity;
}
Le thread peut traiter d’autres requêtes pendant l’attente I/O (DB, HTTP…).
• Task : type principal pour représenter une opération asynchrone. Permet d’attendre avec await, de composer, de gérer les exceptions.
• ValueTask : version plus légère pour certaines opérations très fréquentes (évite des allocations de Task lorsque le résultat est souvent déjà disponible).
• async void : à éviter sauf pour les handlers d’événements UI, car on ne peut pas await ni gérer correctement les exceptions (elles remontent au SynchronizationContext).
Un middleware est un composant du pipeline HTTP qui peut inspecter / modifier la requête et la réponse (auth, logging, compression, etc.).
Exemple de middleware minimal dans Program.cs :
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
var path = context.Request.Path;
Console.WriteLine($"[Request] {path} - {sw.ElapsedMilliseconds} ms");
});
Les middlewares sont exécutés dans l’ordre où tu appelles `app.Use(...)` / `app.UseRouting()` / `app.UseAuthentication()`…
Les Minimal APIs permettent de définir des endpoints HTTP de façon très compacte, sans contrôleurs MVC, idéal pour des microservices.
Exemple :
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/ping", () => "pong");
app.MapGet("/api/users/{id:int}", async (int id, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id);
return user is null ? Results.NotFound() : Results.Ok(user);
});
app.Run();
• `appsettings.json` + `appsettings.Development.json` pour la config classique.
• Variables d’environnement pour surcharger en dev / CI / prod.
• Secret Manager en local (`dotnet user-secrets`) pour les secrets.
• Store de secrets en prod (Azure Key Vault, AWS Secrets Manager…).
On injecte la configuration via `IConfiguration` ou `IOptions` et on évite de commiter les secrets dans Git.
ASP.NET Core fournit un conteneur DI intégré. Tu enregistres tes services dans Program.cs puis tu les reçois via le constructeur ou les handlers.
Exemple :
// Program.cs
builder.Services.AddScoped<IMailService, MailService>();
app.MapPost("/contact", async (ContactDto dto, IMailService mail) =>
{
await mail.SendAsync(dto.Email, "Contact", dto.Message);
return Results.Ok();
});
Le runtime crée et injecte automatiquement la bonne instance de `IMailService`.
EF Core est un ORM qui mappe des classes .NET vers des tables de base de données.
Exemple de DbContext :
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<User> Users => Set<User>();
}
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
}
On crée ensuite des migrations (`dotnet ef migrations add Initial`) puis on met à jour la base (`dotnet ef database update`).
• Pour contrôler exactement ce qui sort de l’API (ne pas exposer des colonnes sensibles).
• Pour découpler le modèle de persistance du contrat API (éviter les breaking changes si la base change).
• Pour optimiser la payload (exposer seulement les champs nécessaires).
On peut mapper avec AutoMapper ou à la main via des sélects LINQ.
On peut créer un middleware de gestion d’exceptions qui transforme toutes les exceptions non gérées en réponses HTTP propres (400, 404, 500…).
Exemple simplifié :
app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch (NotFoundException)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
catch (Exception ex)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
// log ex
}
});
Cela évite de répéter les `try/catch` dans chaque endpoint.
Étapes classiques :
1. Endpoint `/auth/login` qui valide les identifiants et génère un JWT signé (HMAC ou RSA).
2. Configuration de la validation dans Program.cs via `AddAuthentication().AddJwtBearer(...)`.
3. Ajout de `[Authorize]` sur les endpoints protégés.
Exemple de configuration :
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)
)
};
});
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
On sépare strictement :
• Domain : entités métier, Value Objects, règles métier.
• Application : use cases, ports, interfaces de repository.
• Infrastructure : implémentations concrètes (EF Core, HTTP, email).
• UI / API : contrôleurs, endpoints, front.
Les dépendances vont uniquement vers le cœur (Domain & Application). Ça permet :
• de tester le cœur sans la base / le front,
• de changer d’UI ou de techno de stockage plus facilement (par ex. remplacer EF Core par Dapper).
• Logging structuré via `ILogger` ou une lib comme Serilog.
• Traces distribuées (OpenTelemetry) pour suivre un appel de bout en bout.
• Metrics pour suivre les temps de réponse, erreurs, throughput.
Exemple très simplifié avec OpenTelemetry :
builder.Services.AddOpenTelemetry()
.WithTracing(b => b
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation())
.WithMetrics(b => b
.AddAspNetCoreInstrumentation());
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
Ensuite tu envoies ça vers un backend d’observabilité (Jaeger, Tempo, Application Insights, etc.).
Native AOT (Ahead Of Time) compile ton application en binaire natif, sans JIT au runtime.
Avantages :
• démarrage très rapide (cold start faible),
• empreinte mémoire réduite,
• idéal pour des microservices serverless ou des CLI.
Inconvénients : certaines fonctionnalités dynamiques (reflection lourde, runtime codegen) sont limitées ou nécessitent de la configuration supplémentaire.
Quelques piliers :
• Toujours HTTPS + HSTS, configuration de Kestrel / reverse proxy correcte.
• Authentification robuste (JWT, OAuth2, OpenID Connect).
• Validation stricte des entrées (FluentValidation, annotations, regex).
• Rate limiting, protection contre brute force et enumeration (ASP.NET RateLimiter).
• Logs de sécurité + alertes (connexion suspecte, erreurs 401/403 massives).
• Mises à jour régulières des dépendances, scans de vulnérabilités (Dependabot, Snyk…).
• Scalabilité horizontale : conteneurs Docker + orchestrateur (Kubernetes, Azure App Service…).
• Caching : en mémoire pour le très chaud (IMemoryCache), distribué (Redis) et CDN pour le statique.
• Base de données : index adaptés, requêtes optimisées, pooling des connexions.
• Async partout pour les I/O (EF async, HttpClient async…).
• Profiling régulier (dotnet-trace, PerfView, Application Insights) pour identifier les hotspots.
CQRS (Command Query Responsibility Segregation) sépare :
• les commandes (écriture) — modifier l’état,
• les requêtes (lecture) — retourner des données, sans effet de bord.
En .NET on combine souvent CQRS avec MediatR :
• chaque commande / requête est un record (`IRequest`),
• chaque handler implémente la logique.
On l’utilise sur des systèmes métier complexes, avec des règles de validation fortes ou lorsque la partie lecture doit être très optimisée.
• Monolithe modulaire : une seule application déployée, mais structurée en modules / bounded contexts bien séparés (par exemple plusieurs projets .NET dans une même solution Web).
• Microservices : plusieurs services indépendants, chacun avec sa base, son déploiement et son cycle de vie.
Un monolithe modulaire est souvent plus simple à démarrer / maintenir au début, puis tu extrais certains modules en microservices lorsqu’il y a un vrai besoin (scalabilité, frontières métier claires, équipes séparées).
REST (JSON + HTTP) :
• excellent pour des APIs publiques ou web,
• facilement consommable depuis un navigateur, mobile, etc.
gRPC :
• basé sur HTTP/2 + Protobuf,
• très performant, bi-directionnel (streaming),
• idéal pour la communication inter-services (backend-to-backend).
En .NET :
• gRPC se configure via `AddGrpc()` et `MapGrpcService`.
• REST via Minimal APIs / contrôleurs.
On garde souvent REST pour l’externe et gRPC pour le trafic interne.
Quelques bonnes pratiques :
• Toujours utiliser les versions async (`ToListAsync`, `FirstOrDefaultAsync`…).
• Utiliser `AsNoTracking()` pour les lectures qui ne modifient pas les entités.
• Projeter directement vers des DTO (`Select`) au lieu de charger l’entité complète.
• Ajouter les bons index en base (vérifier les plans d’exécution).
• Limiter la taille des collections chargées (pagination, filtres).
Exemple :
var users = await _db.Users
.AsNoTracking()
.Where(u => u.IsActive)
.OrderBy(u => u.CreatedAt)
.Select(u => new UserListItemDto(u.Id, u.Email))
.Take(50)
.ToListAsync();
• Tests unitaires : ciblent une classe ou un use case, sans dépendances externes (mocks / fakes). Ex : règles métier dans la couche Domain.
• Tests d’intégration : démarrent l’API en mémoire (WebApplicationFactory), utilisent une vraie base (ou SQLite in-memory) pour tester un scénario complet.
• Tests end-to-end : passent par l’API depuis l’extérieur (ou via l’UI) pour reproduire un parcours utilisateur.
L’idée est d’avoir surtout beaucoup de tests unitaires, quelques tests d’intégration bien choisis et peu mais pertinents end-to-end.