# Questions d'entretien C# et .NET : Guide complet 2026 > Les 25 questions d'entretien C# et .NET les plus fréquentes. LINQ, async/await, dependency injection, Entity Framework et bonnes pratiques avec réponses détaillées. - Published: 2026-01-25 - Updated: 2026-04-01 - Author: SharpSkill - Tags: csharp, dotnet, interview, aspnet core, entretien technique - Reading time: 20 min --- Les entretiens C# et .NET évaluent la maîtrise du langage, la compréhension de l'écosystème Microsoft et la capacité à concevoir des applications robustes et performantes. Ce guide couvre les questions essentielles, des fondamentaux du langage jusqu'aux patterns avancés d'architecture. > **Conseil pour l'entretien** > > Les recruteurs valorisent les réponses qui démontrent une compréhension des mécanismes internes de .NET, pas seulement la syntaxe. Expliquer le "pourquoi" derrière chaque concept fait la différence. ## Fondamentaux C# ### Question 1 : Quelle est la différence entre value types et reference types ? Cette distinction fondamentale affecte l'allocation mémoire, les performances et le comportement lors des passages de paramètres. ```csharp // ValueVsReference.cs // Démonstration des différences de comportement // VALUE TYPES : stockés sur la Stack, copiés par valeur struct Point { public int X; public int Y; } // REFERENCE TYPES : stockés sur le Heap, copiés par référence class Person { public string Name; } public class Demo { public static void Main() { // Value type : copie indépendante Point p1 = new Point { X = 10, Y = 20 }; Point p2 = p1; // Copie complète des valeurs p2.X = 100; // Ne modifie PAS p1 Console.WriteLine($"p1.X = {p1.X}"); // 10 // Reference type : même objet en mémoire Person person1 = new Person { Name = "Alice" }; Person person2 = person1; // Copie de la référence person2.Name = "Bob"; // MODIFIE person1 aussi Console.WriteLine($"person1.Name = {person1.Name}"); // Bob // Cas particulier : string est immutable string s1 = "Hello"; string s2 = s1; s2 = "World"; // Crée une nouvelle string Console.WriteLine($"s1 = {s1}"); // Hello } } ``` Les value types (int, struct, enum) sont alloués sur la Stack et libérés automatiquement. Les reference types (class, interface, delegate) sont alloués sur le Heap et gérés par le Garbage Collector. ### Question 2 : Expliquez le mot-clé `ref`, `out` et `in` Ces modificateurs contrôlent comment les paramètres sont passés aux méthodes, avec des implications sur les performances et la mutabilité. ```csharp // ParameterModifiers.cs // Les trois modificateurs de passage par référence public class ParameterDemo { // REF : la variable DOIT être initialisée avant l'appel // Peut être lue ET modifiée dans la méthode public static void ModifyWithRef(ref int value) { Console.WriteLine($"Valeur reçue: {value}"); value = value * 2; // Modification visible à l'appelant } // OUT : la variable n'a PAS besoin d'être initialisée // DOIT être assignée avant la fin de la méthode public static bool TryParse(string input, out int result) { // result DOIT être assigné dans tous les chemins d'exécution if (int.TryParse(input, out result)) { return true; } result = 0; // Assignation obligatoire return false; } // IN : passage par référence en lecture seule (C# 7.2+) // Évite la copie pour les gros structs sans permettre la modification public static double CalculateDistance(in Point3D p1, in Point3D p2) { // p1.X = 10; // ERREUR : ne peut pas modifier un paramètre 'in' return Math.Sqrt( Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2) + Math.Pow(p2.Z - p1.Z, 2) ); } public static void Main() { // Utilisation de ref int number = 5; ModifyWithRef(ref number); Console.WriteLine($"Après ref: {number}"); // 10 // Utilisation de out if (TryParse("123", out int parsed)) { Console.WriteLine($"Parsed: {parsed}"); // 123 } // Utilisation de in (optimal pour les gros structs) var point1 = new Point3D(0, 0, 0); var point2 = new Point3D(3, 4, 0); var distance = CalculateDistance(in point1, in point2); } } public readonly struct Point3D { public readonly double X, Y, Z; public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z); } ``` `in` est particulièrement utile pour les structs volumineux car il évite la copie tout en garantissant l'immutabilité. C'est un pattern courant dans le code haute performance. > **Performance avec in** > > Utiliser `in` pour les structs de plus de 16 bytes améliore les performances en évitant la copie. Pour les petits structs, le passage par valeur reste plus efficace. ### Question 3 : Comment fonctionne le Garbage Collector en .NET ? Le GC .NET utilise un algorithme générationnel pour optimiser la gestion mémoire automatique. ```csharp // GarbageCollectorDemo.cs // Comprendre le fonctionnement du GC public class GCDemo { public static void DemonstrateGenerations() { // Génération 0 : objets nouvellement alloués var shortLived = new byte[1000]; Console.WriteLine($"Génération: {GC.GetGeneration(shortLived)}"); // 0 // Forcer une collection pour promouvoir l'objet GC.Collect(); Console.WriteLine($"Après GC: {GC.GetGeneration(shortLived)}"); // 1 GC.Collect(); Console.WriteLine($"Après 2ème GC: {GC.GetGeneration(shortLived)}"); // 2 // Statistiques mémoire var info = GC.GetGCMemoryInfo(); Console.WriteLine($"Heap total: {info.HeapSizeBytes / 1024 / 1024}MB"); } // Pattern IDisposable pour les ressources non-managées public class DatabaseConnection : IDisposable { private IntPtr _nativeHandle; private bool _disposed = false; public DatabaseConnection() { _nativeHandle = AllocateNativeResource(); } // Méthode Dispose publique public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); // Évite l'appel au finalizer } // Pattern Dispose protégé protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { // Libérer les ressources managées } // Libérer les ressources non-managées if (_nativeHandle != IntPtr.Zero) { FreeNativeResource(_nativeHandle); _nativeHandle = IntPtr.Zero; } _disposed = true; } } // Finalizer (destructeur) - appelé par le GC si Dispose n'a pas été appelé ~DatabaseConnection() { Dispose(disposing: false); } private IntPtr AllocateNativeResource() => IntPtr.Zero; private void FreeNativeResource(IntPtr handle) { } } } // Utilisation recommandée avec using public class Usage { public void Example() { // C# 8+ : using declaration using var connection = new GCDemo.DatabaseConnection(); // ... utilisation // Dispose() appelé automatiquement à la fin du scope } } ``` Le GC collecte la Génération 0 fréquemment (millisecondes), la Génération 1 occasionnellement, et la Génération 2 rarement. Les objets LOH (Large Object Heap > 85KB) sont traités séparément. ## LINQ et Collections ### Question 4 : Quelle est la différence entre IEnumerable et IQueryable ? Cette question est cruciale pour comprendre l'exécution différée et les performances des requêtes. ```csharp // EnumerableVsQueryable.cs // Différences fondamentales d'exécution public class LinqDemo { public static void CompareExecution(AppDbContext context) { // IEnumerable : exécution EN MÉMOIRE (client-side) IEnumerable enumerable = context.Products.AsEnumerable(); var filteredEnum = enumerable .Where(p => p.Price > 100) // Filtrage en C# .ToList(); // SQL généré : SELECT * FROM Products (TOUT chargé) // IQueryable : exécution côté BASE DE DONNÉES (server-side) IQueryable queryable = context.Products; var filteredQuery = queryable .Where(p => p.Price > 100) // Traduit en SQL WHERE .ToList(); // SQL généré : SELECT * FROM Products WHERE Price > 100 // Composition de requêtes avec IQueryable var query = context.Products.AsQueryable(); // Chaque opération ajoute à l'Expression Tree query = query.Where(p => p.IsActive); query = query.Where(p => p.CategoryId == 5); query = query.OrderBy(p => p.Name); // L'exécution se fait ICI, avec une seule requête SQL optimisée var results = query.ToList(); } // Méthode générique qui fonctionne avec les deux public static IEnumerable FilterByCondition( IEnumerable source, Func predicate) { return source.Where(predicate); } // Version optimisée pour IQueryable public static IQueryable FilterByCondition( IQueryable source, Expression> predicate) { // Expression> permet la traduction en SQL return source.Where(predicate); } } ``` Utiliser `IQueryable` avec Entity Framework pour que le filtrage soit fait côté base de données. `IEnumerable` convient pour les collections en mémoire ou quand toutes les données sont déjà chargées. ### Question 5 : Expliquez l'exécution différée (deferred execution) en LINQ L'exécution différée est un concept fondamental qui affecte les performances et le comportement des requêtes LINQ. ```csharp // DeferredExecution.cs // Comprendre quand les requêtes s'exécutent réellement public class DeferredExecutionDemo { public static void Demonstrate() { var numbers = new List { 1, 2, 3, 4, 5 }; // La requête est DÉFINIE mais PAS EXÉCUTÉE var query = numbers.Where(n => { Console.WriteLine($"Évaluation de {n}"); return n > 2; }); Console.WriteLine("Requête définie, mais rien ne s'est passé"); // Modification de la source AVANT l'exécution numbers.Add(6); numbers.Add(7); Console.WriteLine("Début de l'itération:"); // L'EXÉCUTION se fait ICI lors de l'énumération foreach (var n in query) { Console.WriteLine($"Résultat: {n}"); } // Sortie inclut 6 et 7 car ils ont été ajoutés avant l'exécution } // Méthodes qui FORCENT l'exécution immédiate public static void ImmediateExecution() { var numbers = new List { 1, 2, 3, 4, 5 }; // ToList(), ToArray(), ToDictionary() = exécution immédiate var list = numbers.Where(n => n > 2).ToList(); // Count(), First(), Single(), Any() = exécution immédiate var count = numbers.Where(n => n > 2).Count(); var first = numbers.First(n => n > 2); // Aggregate(), Sum(), Max(), Min() = exécution immédiate var sum = numbers.Where(n => n > 2).Sum(); } // Danger : multiple énumération public static void MultipleEnumerationProblem() { var numbers = GetNumbers(); // IEnumerable retourné par yield // ⚠️ CHAQUE utilisation ré-exécute la requête var count = numbers.Count(); // 1ère énumération var first = numbers.First(); // 2ème énumération // ✅ Solution : matérialiser une fois var materializedList = numbers.ToList(); var countOk = materializedList.Count; // Pas de ré-exécution var firstOk = materializedList.First(); // Pas de ré-exécution } private static IEnumerable GetNumbers() { Console.WriteLine("GetNumbers appelé"); yield return 1; yield return 2; yield return 3; } } ``` > **Multiple énumération** > > Utiliser un analyzer comme ReSharper ou Rider pour détecter les problèmes de multiple énumération qui peuvent causer des bugs subtils et des problèmes de performance. ## Async/Await et Multithreading ### Question 6 : Expliquez async/await et le fonctionnement des Tasks L'asynchronisme est essentiel pour les applications modernes. Comprendre son fonctionnement interne démontre une expertise avancée. ```csharp // AsyncAwaitDemo.cs // Mécanismes internes de l'asynchronisme public class AsyncDemo { // async transforme la méthode en state machine public async Task FetchDataAsync(string url) { using var client = new HttpClient(); // await libère le thread pendant l'attente I/O // Le thread retourne au pool et peut traiter d'autres requêtes var response = await client.GetStringAsync(url); // Après await, l'exécution reprend (possiblement sur un autre thread) return ProcessData(response); } // Pattern pour exécution parallèle public async Task<(User, List)> GetUserWithOrdersAsync(int userId) { // Les deux appels démarrent SIMULTANÉMENT var userTask = GetUserAsync(userId); var ordersTask = GetOrdersAsync(userId); // await attend les deux résultats await Task.WhenAll(userTask, ordersTask); return (userTask.Result, ordersTask.Result); } // ConfigureAwait pour les bibliothèques public async Task LibraryMethodAsync() { // ConfigureAwait(false) évite de capturer le SynchronizationContext // Recommandé dans les bibliothèques pour éviter les deadlocks var data = await FetchDataAsync("https://api.example.com") .ConfigureAwait(false); return data.ToUpper(); } // ❌ Anti-pattern : async void (sauf pour les event handlers) public async void BadAsyncMethod() { // Les exceptions ne peuvent pas être catchées // Impossible d'attendre la complétion await Task.Delay(100); } // ✅ Correct : async Task public async Task GoodAsyncMethod() { await Task.Delay(100); } private Task GetUserAsync(int id) => Task.FromResult(new User()); private Task> GetOrdersAsync(int id) => Task.FromResult(new List()); private string ProcessData(string data) => data; } public class User { } public class Order { } ``` Le compilateur transforme les méthodes async en state machines. Chaque `await` représente un point de suspension où le thread est libéré. ### Question 7 : Comment éviter les deadlocks avec async/await ? Les deadlocks asynchrones sont un piège classique, surtout dans les applications avec un SynchronizationContext (UI, ASP.NET classique). ```csharp // DeadlockPrevention.cs // Patterns pour éviter les deadlocks public class DeadlockDemo { private readonly IDataService _service; // ❌ DEADLOCK dans ASP.NET classique ou WinForms/WPF public string GetDataDeadlock() { // .Result ou .Wait() bloque le thread UI/Request // async essaie de reprendre sur ce même thread = deadlock return _service.FetchAsync().Result; } // ✅ Solution 1 : async all the way public async Task GetDataAsync() { return await _service.FetchAsync(); } // ✅ Solution 2 : ConfigureAwait(false) dans la bibliothèque public async Task FetchAsync() { var data = await HttpClient.GetStringAsync("url") .ConfigureAwait(false); // Ne capture pas le contexte return data; } // ✅ Solution 3 : Task.Run pour isoler (si vraiment nécessaire) public string GetDataWithTaskRun() { // S'exécute sur un thread pool sans SynchronizationContext return Task.Run(async () => await _service.FetchAsync()).Result; } // Pattern pour cancellation propre public async Task FetchWithCancellation(CancellationToken cancellationToken) { using var client = new HttpClient(); try { var response = await client.GetStringAsync("url", cancellationToken); return response; } catch (OperationCanceledException) { // Gérer l'annulation gracieusement return string.Empty; } } // Timeout pattern public async Task FetchWithTimeout(TimeSpan timeout) { using var cts = new CancellationTokenSource(timeout); try { return await FetchWithCancellation(cts.Token); } catch (OperationCanceledException) { throw new TimeoutException("La requête a expiré"); } } private static readonly HttpClient HttpClient = new(); } public interface IDataService { Task FetchAsync(); } ``` La règle d'or : "async all the way". Éviter de mélanger code synchrone et asynchrone. Dans ASP.NET Core, le SynchronizationContext n'existe pas, réduisant les risques de deadlock. ## Dependency Injection et Architecture ### Question 8 : Expliquez les différentes durées de vie en DI (Scoped, Transient, Singleton) La compréhension des lifetimes est essentielle pour éviter les bugs de concurrence et les fuites mémoire. ```csharp // DependencyInjectionLifetimes.cs // Les trois durées de vie et leurs implications // SINGLETON : une seule instance pour toute l'application public class SingletonService { private readonly Guid _id = Guid.NewGuid(); public Guid Id => _id; // ⚠️ DANGER : pas de state mutable sans synchronisation // ❌ private int _counter; // Race conditions possibles } // SCOPED : une instance par requête HTTP (ou scope) public class ScopedService { private readonly Guid _id = Guid.NewGuid(); public Guid Id => _id; // ✅ Safe : chaque requête a sa propre instance // Idéal pour DbContext, UnitOfWork } // TRANSIENT : nouvelle instance à chaque injection public class TransientService { private readonly Guid _id = Guid.NewGuid(); public Guid Id => _id; // Idéal pour les services légers et stateless } // Configuration dans Program.cs public static class ServiceConfiguration { public static void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddScoped(); services.AddTransient(); // Entity Framework : TOUJOURS Scoped services.AddDbContext(options => options.UseSqlServer(connectionString)); // HttpClient : utiliser IHttpClientFactory services.AddHttpClient(); } } // ❌ CAPTIVE DEPENDENCY : Singleton qui dépend d'un Scoped public class BadSingletonService { // ⚠️ Le ScopedService sera capturé et réutilisé indéfiniment // Cause des bugs de concurrence et des données périmées private readonly ScopedService _scoped; public BadSingletonService(ScopedService scoped) { _scoped = scoped; } } // ✅ Solution : utiliser IServiceScopeFactory public class GoodSingletonService { private readonly IServiceScopeFactory _scopeFactory; public GoodSingletonService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task DoWork() { // Créer un scope explicite pour obtenir un ScopedService frais using var scope = _scopeFactory.CreateScope(); var scoped = scope.ServiceProvider.GetRequiredService(); // Utiliser scoped... } } ``` Règle : un service ne doit jamais dépendre d'un service avec une durée de vie plus courte. Singleton → Scoped → Transient. ### Question 9 : Quels sont les principaux design patterns en .NET ? Les recruteurs attendent une connaissance pratique des patterns, pas juste leur définition. ```csharp // DesignPatterns.cs // Patterns courants en C#/.NET // REPOSITORY : abstraction de l'accès aux données public interface IUserRepository { Task GetByIdAsync(int id); Task> GetAllAsync(); Task AddAsync(User user); Task UpdateAsync(User user); Task DeleteAsync(int id); } public class UserRepository : IUserRepository { private readonly AppDbContext _context; public UserRepository(AppDbContext context) => _context = context; public async Task GetByIdAsync(int id) => await _context.Users.FindAsync(id); public async Task> GetAllAsync() => await _context.Users.ToListAsync(); public async Task AddAsync(User user) => await _context.Users.AddAsync(user); public async Task UpdateAsync(User user) => _context.Users.Update(user); public async Task DeleteAsync(int id) { var user = await GetByIdAsync(id); if (user != null) _context.Users.Remove(user); } } // UNIT OF WORK : coordination des transactions public interface IUnitOfWork : IDisposable { IUserRepository Users { get; } IOrderRepository Orders { get; } Task SaveChangesAsync(); } public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; public UnitOfWork(AppDbContext context) { _context = context; Users = new UserRepository(context); Orders = new OrderRepository(context); } public IUserRepository Users { get; } public IOrderRepository Orders { get; } public async Task SaveChangesAsync() => await _context.SaveChangesAsync(); public void Dispose() => _context.Dispose(); } // FACTORY : création d'objets complexes public interface INotificationFactory { INotification Create(NotificationType type); } public class NotificationFactory : INotificationFactory { public INotification Create(NotificationType type) => type switch { NotificationType.Email => new EmailNotification(), NotificationType.Sms => new SmsNotification(), NotificationType.Push => new PushNotification(), _ => throw new ArgumentException($"Type inconnu: {type}") }; } // DECORATOR : ajouter des comportements dynamiquement public interface IUserService { Task GetUserAsync(int id); } public class UserService : IUserService { private readonly IUserRepository _repository; public UserService(IUserRepository repository) => _repository = repository; public async Task GetUserAsync(int id) => await _repository.GetByIdAsync(id) ?? throw new NotFoundException($"User {id} not found"); } // Decorator qui ajoute du caching public class CachedUserService : IUserService { private readonly IUserService _inner; private readonly IMemoryCache _cache; public CachedUserService(IUserService inner, IMemoryCache cache) { _inner = inner; _cache = cache; } public async Task GetUserAsync(int id) { var cacheKey = $"user:{id}"; if (_cache.TryGetValue(cacheKey, out User? cached)) return cached!; var user = await _inner.GetUserAsync(id); _cache.Set(cacheKey, user, TimeSpan.FromMinutes(5)); return user; } } ``` Ces patterns sont utilisés quotidiennement dans les applications .NET professionnelles. Le pattern Repository avec Unit of Work est particulièrement courant avec Entity Framework. ## Entity Framework Core ### Question 10 : Comment optimiser les performances avec EF Core ? EF Core peut être très performant ou très lent selon son utilisation. Cette question évalue la connaissance des bonnes pratiques. ```csharp // EFCoreOptimization.cs // Techniques d'optimisation des requêtes public class EFCorePerformance { private readonly AppDbContext _context; // ❌ Problème N+1 : une requête par order public async Task> GetUsersWithOrdersBad() { var users = await _context.Users.ToListAsync(); foreach (var user in users) { // N requêtes supplémentaires ! var orders = await _context.Orders .Where(o => o.UserId == user.Id) .ToListAsync(); } return users; } // ✅ Eager Loading avec Include public async Task> GetUsersWithOrdersGood() { return await _context.Users .Include(u => u.Orders) // JOIN en SQL .ThenInclude(o => o.Products) // Include imbriqué .ToListAsync(); } // ✅ Projection pour charger uniquement les données nécessaires public async Task> GetUserSummaries() { return await _context.Users .Select(u => new UserDto { Id = u.Id, Name = u.Name, OrderCount = u.Orders.Count, // Calculé côté SQL TotalSpent = u.Orders.Sum(o => o.Total) }) .ToListAsync(); } // ✅ Split Query pour les grandes collections public async Task> GetUsersWithSplitQuery() { return await _context.Users .Include(u => u.Orders) .AsSplitQuery() // Génère des requêtes séparées au lieu d'un gros JOIN .ToListAsync(); } // ✅ No Tracking pour les lectures seules public async Task> GetUsersReadOnly() { return await _context.Users .AsNoTracking() // Pas de change tracking = plus rapide .ToListAsync(); } // ✅ Batch operations (EF Core 7+) public async Task DeleteInactiveUsers() { // Une seule requête DELETE au lieu de charger puis supprimer await _context.Users .Where(u => !u.IsActive && u.LastLoginAt < DateTime.UtcNow.AddYears(-1)) .ExecuteDeleteAsync(); } // ✅ Bulk update public async Task DeactivateOldUsers() { await _context.Users .Where(u => u.LastLoginAt < DateTime.UtcNow.AddMonths(-6)) .ExecuteUpdateAsync(u => u.SetProperty(x => x.IsActive, false)); } // ✅ Compiled Queries pour les requêtes fréquentes private static readonly Func> GetUserById = EF.CompileAsyncQuery((AppDbContext ctx, int id) => ctx.Users.FirstOrDefault(u => u.Id == id)); public async Task GetUserOptimized(int id) { return await GetUserById(_context, id); } } ``` > **Monitoring des requêtes** > > Activer le logging SQL en développement avec `optionsBuilder.LogTo(Console.WriteLine)` pour identifier les requêtes problématiques. En production, utiliser des outils comme MiniProfiler ou Application Insights. ### Question 11 : Expliquez les migrations et la gestion du schéma La gestion des migrations est critique pour les déploiements en production. ```csharp // MigrationStrategies.cs // Gestion professionnelle des migrations EF Core // Configuration du DbContext avec conventions public class AppDbContext : DbContext { public DbSet Users => Set(); public DbSet Orders => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { // Appliquer toutes les configurations IEntityTypeConfiguration modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); // Convention globale pour les dates foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { if (property.ClrType == typeof(DateTime)) { property.SetColumnType("datetime2"); } } } } } // Configuration fluide séparée public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Users"); builder.HasKey(u => u.Id); builder.Property(u => u.Email) .IsRequired() .HasMaxLength(256); builder.HasIndex(u => u.Email) .IsUnique(); builder.HasMany(u => u.Orders) .WithOne(o => o.User) .HasForeignKey(o => o.UserId) .OnDelete(DeleteBehavior.Cascade); } } // Seeding de données public class DataSeeder { public static void Seed(ModelBuilder modelBuilder) { modelBuilder.Entity().HasData( new Role { Id = 1, Name = "Admin" }, new Role { Id = 2, Name = "User" } ); } } ``` Commandes de migration essentielles : - `dotnet ef migrations add NomMigration` - Créer une migration - `dotnet ef database update` - Appliquer les migrations - `dotnet ef migrations script` - Générer le script SQL - `dotnet ef migrations remove` - Supprimer la dernière migration ## ASP.NET Core ### Question 12 : Expliquez le pipeline de middleware ASP.NET Core Le pipeline de middleware est le cœur d'ASP.NET Core. Comprendre son fonctionnement est essentiel. ```csharp // MiddlewarePipeline.cs // Architecture du pipeline de requêtes // Custom Middleware - classe complète public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public RequestLoggingMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { // AVANT : exécuté à l'aller (request) var stopwatch = Stopwatch.StartNew(); _logger.LogInformation("Request: {Method} {Path}", context.Request.Method, context.Request.Path); try { // Passer au middleware suivant await _next(context); } finally { // APRÈS : exécuté au retour (response) stopwatch.Stop(); _logger.LogInformation("Response: {StatusCode} in {ElapsedMs}ms", context.Response.StatusCode, stopwatch.ElapsedMilliseconds); } } } // Extension pour l'enregistrement public static class MiddlewareExtensions { public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app) { return app.UseMiddleware(); } } // Configuration du pipeline dans Program.cs public class Startup { public void Configure(IApplicationBuilder app) { // L'ORDRE est CRITIQUE ! // 1. Gestion des exceptions (doit être en premier) app.UseExceptionHandler("/error"); // 2. HTTPS Redirection app.UseHttpsRedirection(); // 3. Fichiers statiques (court-circuite si trouvé) app.UseStaticFiles(); // 4. Routing (détermine l'endpoint) app.UseRouting(); // 5. CORS (doit être entre Routing et Auth) app.UseCors(); // 6. Authentication (qui êtes-vous ?) app.UseAuthentication(); // 7. Authorization (avez-vous le droit ?) app.UseAuthorization(); // 8. Custom middleware app.UseRequestLogging(); // 9. Endpoints (exécute le controller/action) app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapRazorPages(); }); } } // Middleware conditionnel public static class ConditionalMiddleware { public static IApplicationBuilder UseWhen( this IApplicationBuilder app, Func predicate, Action configuration) { // Branche conditionnelle du pipeline return app.UseWhen(predicate, configuration); } public static void Example(IApplicationBuilder app) { // Appliquer un middleware seulement pour /api/* app.UseWhen( context => context.Request.Path.StartsWithSegments("/api"), apiApp => apiApp.UseMiddleware() ); } } ``` Le middleware s'exécute dans l'ordre d'enregistrement à l'aller (request) et dans l'ordre inverse au retour (response). ### Question 13 : Comment implémenter l'authentification JWT ? L'authentification JWT est le standard pour les APIs REST modernes. ```csharp // JwtAuthentication.cs // Configuration complète de l'authentification JWT public static class JwtConfiguration { public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration config) { var jwtSettings = config.GetSection("Jwt").Get()!; services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Audience, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(jwtSettings.SecretKey)), ClockSkew = TimeSpan.Zero // Pas de tolérance sur l'expiration }; // Événements pour logging/debugging options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception is SecurityTokenExpiredException) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); } } public class JwtSettings { public string SecretKey { get; set; } = string.Empty; public string Issuer { get; set; } = string.Empty; public string Audience { get; set; } = string.Empty; public int ExpirationMinutes { get; set; } = 60; } // Service de génération de tokens public class TokenService { private readonly JwtSettings _settings; public TokenService(IOptions settings) { _settings = settings.Value; } public string GenerateToken(User user, IEnumerable roles) { var securityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_settings.SecretKey)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.Email, user.Email), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new("name", user.Name) }; // Ajouter les rôles comme claims claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); var token = new JwtSecurityToken( issuer: _settings.Issuer, audience: _settings.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_settings.ExpirationMinutes), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } public ClaimsPrincipal? ValidateToken(string token) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(_settings.SecretKey); try { var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidIssuer = _settings.Issuer, ValidateAudience = true, ValidAudience = _settings.Audience, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }, out _); return principal; } catch { return null; } } } // Utilisation dans un controller [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly TokenService _tokenService; private readonly IUserService _userService; [HttpPost("login")] public async Task Login([FromBody] LoginDto dto) { var user = await _userService.ValidateCredentialsAsync(dto.Email, dto.Password); if (user == null) return Unauthorized(new { message = "Invalid credentials" }); var roles = await _userService.GetRolesAsync(user.Id); var token = _tokenService.GenerateToken(user, roles); return Ok(new { token, expiresIn = 3600 }); } [Authorize] // Requiert un token valide [HttpGet("profile")] public IActionResult GetProfile() { var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; return Ok(new { userId }); } [Authorize(Roles = "Admin")] // Requiert le rôle Admin [HttpGet("admin")] public IActionResult AdminOnly() { return Ok(new { message = "Welcome, Admin!" }); } } ``` ## Questions avancées ### Question 14 : Qu'est-ce que Span`` et Memory`` ? Ces types permettent de manipuler des zones de mémoire sans allocation, essentiels pour le code haute performance. ```csharp // SpanAndMemory.cs // Types pour la manipulation mémoire performante public class HighPerformanceDemo { // Span`` : vue sur une zone mémoire contiguë (stack only) public static void SpanBasics() { // Span sur un tableau int[] numbers = { 1, 2, 3, 4, 5 }; Span span = numbers.AsSpan(); // Slice sans allocation Span slice = span.Slice(1, 3); // [2, 3, 4] // Modification affecte le tableau original slice[0] = 100; Console.WriteLine(numbers[1]); // 100 // Span sur la stack (stackalloc) Span stackSpan = stackalloc int[100]; stackSpan.Fill(42); } // Parsing sans allocation avec Span public static bool TryParseDate(ReadOnlySpan input, out DateTime date) { // Format: "2024-01-15" date = default; if (input.Length != 10) return false; // Slicing sans créer de nouvelles strings var yearSpan = input.Slice(0, 4); var monthSpan = input.Slice(5, 2); var daySpan = input.Slice(8, 2); if (!int.TryParse(yearSpan, out int year)) return false; if (!int.TryParse(monthSpan, out int month)) return false; if (!int.TryParse(daySpan, out int day)) return false; date = new DateTime(year, month, day); return true; } // Memory`` : comme Span mais peut être stocké sur le heap public async Task ProcessDataAsync(Memory buffer) { // Memory peut traverser les frontières async await Task.Delay(100); // Convertir en Span pour le traitement Span span = buffer.Span; int sum = 0; foreach (var b in span) { sum += b; } return sum; } // ArrayPool : réutilisation de tableaux pour éviter les allocations public static void UseArrayPool() { // Louer un tableau du pool byte[] buffer = ArrayPool.Shared.Rent(1024); try { // Utiliser le buffer... // Note : peut être plus grand que demandé Console.WriteLine($"Buffer size: {buffer.Length}"); } finally { // TOUJOURS retourner au pool ArrayPool.Shared.Return(buffer, clearArray: true); } } // Benchmark comparatif public static string SubstringTraditional(string input, int start, int length) { // Crée une nouvelle string = allocation return input.Substring(start, length); } public static ReadOnlySpan SubstringWithSpan(ReadOnlySpan input, int start, int length) { // Retourne une vue = AUCUNE allocation return input.Slice(start, length); } } ``` Span`` est idéal pour le traitement de chaînes, la parsing, et les opérations sur les tableaux sans allocation. ### Question 15 : Expliquez les records et leurs cas d'usage Les records (C# 9+) sont un type de référence immuable avec égalité par valeur, parfaits pour les DTOs et les value objects. ```csharp // RecordsDemo.cs // Fonctionnalités et cas d'usage des records // Record class (référence, immuable par défaut) public record Person(string FirstName, string LastName, DateOnly BirthDate) { // Propriété calculée public int Age => DateTime.Today.Year - BirthDate.Year; // Méthode additionnelle public string FullName => $"{FirstName} {LastName}"; } // Record avec validation public record Email { public string Value { get; } public Email(string value) { if (!IsValidEmail(value)) throw new ArgumentException("Invalid email format"); Value = value; } private static bool IsValidEmail(string email) => !string.IsNullOrEmpty(email) && email.Contains('@'); } // Record struct (valeur, C# 10+) public readonly record struct Point(double X, double Y) { public double Distance => Math.Sqrt(X * X + Y * Y); } public class RecordUsageDemo { public void DemonstrateFeatures() { // Création var person1 = new Person("John", "Doe", new DateOnly(1990, 5, 15)); // Égalité par valeur (pas par référence) var person2 = new Person("John", "Doe", new DateOnly(1990, 5, 15)); Console.WriteLine(person1 == person2); // True // Mutation avec 'with' (crée une copie) var person3 = person1 with { LastName = "Smith" }; Console.WriteLine(person1.LastName); // "Doe" (inchangé) Console.WriteLine(person3.LastName); // "Smith" // Deconstruction var (firstName, lastName, _) = person1; Console.WriteLine($"{firstName} {lastName}"); // ToString() auto-généré Console.WriteLine(person1); // Output: Person { FirstName = John, LastName = Doe, BirthDate = 15/05/1990 } } // Records comme DTOs (transfert de données) public record CreateUserRequest(string Email, string Password, string Name); public record UserResponse(int Id, string Email, string Name, DateTime CreatedAt); // Records comme Value Objects (DDD) public record Money(decimal Amount, string Currency) { public static Money operator +(Money a, Money b) { if (a.Currency != b.Currency) throw new InvalidOperationException("Currency mismatch"); return new Money(a.Amount + b.Amount, a.Currency); } } // Record avec héritage public abstract record Shape(string Color); public record Circle(string Color, double Radius) : Shape(Color); public record Rectangle(string Color, double Width, double Height) : Shape(Color); } ``` Les records sont idéaux pour : DTOs, Value Objects, configurations immuables, et tout objet où l'identité est basée sur les valeurs plutôt que la référence. ### Question 16 : Comment implémenter un système de cache distribué ? Le caching est essentiel pour les performances des applications à grande échelle. ```csharp // DistributedCaching.cs // Implémentation de cache avec Redis public interface ICacheService { Task GetAsync(string key); Task SetAsync(string key, T value, TimeSpan? expiration = null); Task RemoveAsync(string key); Task GetOrSetAsync(string key, Func> factory, TimeSpan? expiration = null); } public class RedisCacheService : ICacheService { private readonly IDistributedCache _cache; private readonly JsonSerializerOptions _jsonOptions; public RedisCacheService(IDistributedCache cache) { _cache = cache; _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; } public async Task GetAsync(string key) { var data = await _cache.GetStringAsync(key); if (string.IsNullOrEmpty(data)) return default; return JsonSerializer.Deserialize(data, _jsonOptions); } public async Task SetAsync(string key, T value, TimeSpan? expiration = null) { var options = new DistributedCacheEntryOptions(); if (expiration.HasValue) { options.AbsoluteExpirationRelativeToNow = expiration; } else { options.SlidingExpiration = TimeSpan.FromMinutes(10); } var json = JsonSerializer.Serialize(value, _jsonOptions); await _cache.SetStringAsync(key, json, options); } public async Task RemoveAsync(string key) { await _cache.RemoveAsync(key); } // Pattern Cache-Aside avec factory public async Task GetOrSetAsync( string key, Func> factory, TimeSpan? expiration = null) { var cached = await GetAsync(key); if (cached != null) return cached; var value = await factory(); await SetAsync(key, value, expiration); return value; } } // Utilisation dans un service public class ProductService { private readonly ICacheService _cache; private readonly IProductRepository _repository; public ProductService(ICacheService cache, IProductRepository repository) { _cache = cache; _repository = repository; } public async Task GetProductAsync(int id) { var cacheKey = $"product:{id}"; return await _cache.GetOrSetAsync( cacheKey, async () => await _repository.GetByIdAsync(id), TimeSpan.FromMinutes(30) ); } // Cache invalidation public async Task UpdateProductAsync(int id, UpdateProductDto dto) { await _repository.UpdateAsync(id, dto); // Invalider le cache await _cache.RemoveAsync($"product:{id}"); } } // Configuration dans Program.cs public static class CacheConfiguration { public static void AddCaching(this IServiceCollection services, IConfiguration config) { services.AddStackExchangeRedisCache(options => { options.Configuration = config.GetConnectionString("Redis"); options.InstanceName = "MyApp:"; }); services.AddSingleton(); } } ``` > **Cache Invalidation** > > "There are only two hard things in Computer Science: cache invalidation and naming things." Définir une stratégie claire d'invalidation du cache est essentiel pour éviter les données périmées. ### Question 17 : Comment gérer les transactions distribuées ? Dans les architectures microservices, les transactions distribuées nécessitent des patterns spécifiques. ```csharp // DistributedTransactions.cs // Patterns pour la cohérence dans les systèmes distribués // Pattern SAGA avec Orchestration public class OrderSaga { private readonly IOrderRepository _orderRepository; private readonly IPaymentService _paymentService; private readonly IInventoryService _inventoryService; private readonly INotificationService _notificationService; public async Task ProcessOrderAsync(CreateOrderCommand command) { Order? order = null; PaymentResult? payment = null; InventoryReservation? reservation = null; try { // Étape 1 : Créer la commande order = await _orderRepository.CreateAsync(command); // Étape 2 : Réserver l'inventaire reservation = await _inventoryService.ReserveAsync(order.Items); // Étape 3 : Traiter le paiement payment = await _paymentService.ProcessAsync(order.Total, command.PaymentMethod); // Étape 4 : Confirmer la commande await _orderRepository.ConfirmAsync(order.Id); // Étape 5 : Notification (non-critique) await _notificationService.SendOrderConfirmationAsync(order); return OrderResult.Success(order.Id); } catch (Exception ex) { // COMPENSATION : annuler les étapes précédentes dans l'ordre inverse if (payment?.IsSuccessful == true) { await _paymentService.RefundAsync(payment.TransactionId); } if (reservation != null) { await _inventoryService.ReleaseReservationAsync(reservation.Id); } if (order != null) { await _orderRepository.CancelAsync(order.Id, ex.Message); } return OrderResult.Failure(ex.Message); } } } // Pattern Outbox pour la publication fiable d'événements public class OutboxProcessor { private readonly AppDbContext _context; private readonly IMessageBus _messageBus; public async Task ProcessOutboxAsync() { var pendingMessages = await _context.OutboxMessages .Where(m => m.ProcessedAt == null) .OrderBy(m => m.CreatedAt) .Take(100) .ToListAsync(); foreach (var message in pendingMessages) { try { // Publier le message await _messageBus.PublishAsync(message.Type, message.Payload); // Marquer comme traité message.ProcessedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } catch (Exception ex) { message.RetryCount++; message.Error = ex.Message; await _context.SaveChangesAsync(); } } } } // Modèle Outbox public class OutboxMessage { public Guid Id { get; set; } public string Type { get; set; } = string.Empty; public string Payload { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } public DateTime? ProcessedAt { get; set; } public int RetryCount { get; set; } public string? Error { get; set; } } // Extension pour ajouter un message à l'outbox dans une transaction public static class DbContextExtensions { public static void AddOutboxMessage(this AppDbContext context, T @event) { var message = new OutboxMessage { Id = Guid.NewGuid(), Type = typeof(T).Name, Payload = JsonSerializer.Serialize(@event), CreatedAt = DateTime.UtcNow }; context.OutboxMessages.Add(message); } } ``` Le pattern SAGA garantit la cohérence éventuelle dans les systèmes distribués. Le pattern Outbox assure la publication fiable des événements même en cas de panne. ## Conclusion Les entretiens C# et .NET évaluent une combinaison de connaissances théoriques sur le runtime et le langage, et de compétences pratiques en architecture et développement d'applications. Maîtriser les concepts fondamentaux tout en comprenant les patterns avancés distingue les développeurs seniors. ### Checklist de préparation - ✅ Comprendre la différence entre value types et reference types - ✅ Maîtriser async/await et éviter les deadlocks - ✅ Connaître les différences entre IEnumerable et IQueryable - ✅ Optimiser les requêtes Entity Framework Core - ✅ Implémenter correctement le pattern IDisposable - ✅ Configurer l'injection de dépendances avec les bons lifetimes - ✅ Sécuriser les APIs avec JWT - ✅ Utiliser Span`` et Memory`` pour le code haute performance La préparation doit combiner théorie et pratique. Construire des projets personnels, contribuer à l'écosystème open source .NET, et résoudre des exercices sur des plateformes comme HackerRank ou LeetCode consolide ces connaissances pour les entretiens les plus exigeants. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/dotnet/questions-entretien-csharp-dotnet