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.

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.
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.
// 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é.
// 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.
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.
// 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.
// Différences fondamentales d'exécution
public class LinqDemo
{
public static void CompareExecution(AppDbContext context)
{
// IEnumerable : exécution EN MÉMOIRE (client-side)
IEnumerable<Product> 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<Product> 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<T> FilterByCondition<T>(
IEnumerable<T> source,
Func<T, bool> predicate)
{
return source.Where(predicate);
}
// Version optimisée pour IQueryable
public static IQueryable<T> FilterByCondition<T>(
IQueryable<T> source,
Expression<Func<T, bool>> predicate)
{
// Expression<Func<>> 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.
// Comprendre quand les requêtes s'exécutent réellement
public class DeferredExecutionDemo
{
public static void Demonstrate()
{
var numbers = new List<int> { 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<int> { 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<int> GetNumbers()
{
Console.WriteLine("GetNumbers appelé");
yield return 1;
yield return 2;
yield return 3;
}
}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.
// Mécanismes internes de l'asynchronisme
public class AsyncDemo
{
// async transforme la méthode en state machine
public async Task<string> 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<Order>)> 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<string> 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<User> GetUserAsync(int id) => Task.FromResult(new User());
private Task<List<Order>> GetOrdersAsync(int id) => Task.FromResult(new List<Order>());
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).
// 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<string> GetDataAsync()
{
return await _service.FetchAsync();
}
// ✅ Solution 2 : ConfigureAwait(false) dans la bibliothèque
public async Task<string> 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<string> 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<string> 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<string> 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.
Prêt à réussir tes entretiens .NET ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
// 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<SingletonService>();
services.AddScoped<ScopedService>();
services.AddTransient<TransientService>();
// Entity Framework : TOUJOURS Scoped
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// HttpClient : utiliser IHttpClientFactory
services.AddHttpClient<IApiClient, ApiClient>();
}
}
// ❌ 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<ScopedService>();
// 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.
// Patterns courants en C#/.NET
// REPOSITORY : abstraction de l'accès aux données
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<IEnumerable<User>> 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<User?> GetByIdAsync(int id)
=> await _context.Users.FindAsync(id);
public async Task<IEnumerable<User>> 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<int> 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<int> 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<User> GetUserAsync(int id);
}
public class UserService : IUserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository) => _repository = repository;
public async Task<User> 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<User> 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.
// 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<List<User>> 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<List<User>> 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<List<UserDto>> 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<List<User>> 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<List<User>> 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<AppDbContext, int, Task<User?>> GetUserById =
EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
ctx.Users.FirstOrDefault(u => u.Id == id));
public async Task<User?> GetUserOptimized(int id)
{
return await GetUserById(_context, id);
}
}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.
// Gestion professionnelle des migrations EF Core
// Configuration du DbContext avec conventions
public class AppDbContext : DbContext
{
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
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<User>
{
public void Configure(EntityTypeBuilder<User> 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<Role>().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 migrationdotnet ef database update- Appliquer les migrationsdotnet ef migrations script- Générer le script SQLdotnet 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.
// Architecture du pipeline de requêtes
// Custom Middleware - classe complète
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> 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<RequestLoggingMiddleware>();
}
}
// 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<HttpContext, bool> predicate,
Action<IApplicationBuilder> 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<ApiRateLimitingMiddleware>()
);
}
}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.
// 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<JwtSettings>()!;
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<JwtSettings> settings)
{
_settings = settings.Value;
}
public string GenerateToken(User user, IEnumerable<string> roles)
{
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_settings.SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
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<IActionResult> 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!" });
}
}Prêt à réussir tes entretiens .NET ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Questions avancées
Question 14 : Qu'est-ce que Span<T> et Memory<T> ?
Ces types permettent de manipuler des zones de mémoire sans allocation, essentiels pour le code haute performance.
// Types pour la manipulation mémoire performante
public class HighPerformanceDemo
{
// Span`<T>` : 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<int> span = numbers.AsSpan();
// Slice sans allocation
Span<int> 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<int> stackSpan = stackalloc int[100];
stackSpan.Fill(42);
}
// Parsing sans allocation avec Span
public static bool TryParseDate(ReadOnlySpan<char> 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`<T>` : comme Span mais peut être stocké sur le heap
public async Task<int> ProcessDataAsync(Memory<byte> buffer)
{
// Memory peut traverser les frontières async
await Task.Delay(100);
// Convertir en Span pour le traitement
Span<byte> 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<byte>.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<byte>.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<char> SubstringWithSpan(ReadOnlySpan<char> input, int start, int length)
{
// Retourne une vue = AUCUNE allocation
return input.Slice(start, length);
}
}Span<T> 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.
// 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.
// Implémentation de cache avec Redis
public interface ICacheService
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
Task RemoveAsync(string key);
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> 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<T?> GetAsync<T>(string key)
{
var data = await _cache.GetStringAsync(key);
if (string.IsNullOrEmpty(data))
return default;
return JsonSerializer.Deserialize<T>(data, _jsonOptions);
}
public async Task SetAsync<T>(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<T> GetOrSetAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiration = null)
{
var cached = await GetAsync<T>(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<Product?> 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<ICacheService, RedisCacheService>();
}
}"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.
// 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<OrderResult> 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<T>(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
<T>et Memory<T>pour le code haute performance
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
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.
Tags
Partager
Articles similaires

Clean Architecture avec .NET : Guide pratique
Maîtriser Clean Architecture en .NET avec C#. Découvrez les principes SOLID, la séparation des couches et les patterns d'implémentation pour des applications maintenables.

.NET 8 : Créer une API avec ASP.NET Core
Guide complet pour créer une API REST professionnelle avec .NET 8 et ASP.NET Core. Controllers, Entity Framework Core, validation et bonnes pratiques expliqués.

Entity Framework Core : Optimisation des Performances et Bonnes Pratiques en 2026
Guide complet sur l'optimisation des performances Entity Framework Core 10 avec .NET 10. AsNoTracking, requêtes compilées, mises à jour par lot, split queries et LeftJoin.