Préparation entretien C# backend

Questions et réponses d'entretien C# 2026

Une sélection ciblée de questions C# pour les entretiens backend, avec des exemples de code courts à expliquer à l'oral.

Commencer un mock interview C# en français Pratique d'entretien technique en anglaisUn mode où les non-natifs peuvent s'entraîner à passer des entretiens techniques.

Types et Bases du Langage

1Comment les types valeur diffèrent-ils des types référence ?

Les variables de type valeur contiennent généralement la valeur elle-même, donc l'assignation copie la valeur. Les variables de type référence contiennent une référence à un objet, donc l'assignation copie la référence et les deux variables peuvent observer les mutations du même objet. La nuance importante est que le stockage dépend de la durée de vie et du contexte, pas seulement du type : une valeur locale à une méthode peut vivre sur la pile, tandis qu'un type valeur stocké comme un champ de classe, un élément de tableau, une capture de closure, un type emballé (boxed value) ou un champ d'état-machine asynchrone vit avec cet objet.

int a = 10;
int b = a;
b = 20;

// a is still 10

var first = new User { Name = "Ann" };
var second = first;
second.Name = "Kate";

// first.Name is also "Kate"

2Comment les classes, structs et records diffèrent-ils ?

Une classe est un type référence avec identité d'objet. Un struct est un type valeur et convient le mieux aux petites valeurs immuables telles que des coordonnées, des plages ou des identifiants. Une classe record est un type référence avec égalité basée sur la valeur ; un struct record est la version de type valeur. Les records sont une syntaxe compilateur au-dessus des types CLR ordinaires qui génère l'égalité, GetHashCode, ToString, la déconstruction et le support de copie, y compris avec des expressions pour les mutations non destructives. Dans le code backend, les records sont utiles pour les modèles de type DTO, tandis que les structs mutables volumineux sont risqués car copier et modifier des copies peut surprendre les lecteurs.

public record User(int Id, string Name);

var first = new User(1, "Alice");
var second = new User(1, "Alice");

Console.WriteLine(first == second); // true

3Comment diffèrent ref, out et in ?

ref passe un argument par référence et exige qu'il soit initialisé avant l'appel ; la méthode peut le lire et le changer. out passe également par référence, mais la méthode doit assigner une valeur avant de retourner. in passe un readonly byref, ce qui peut éviter la copie pour les structs volumineux, mais ce n'est pas une garantie d'immuabilité profonde : des structs mutables et des membres non-readonly peuvent forcer des copies défensives. Au niveau IL, ref, out et in sont gérés par des paramètres byref avec différentes règles C#.

void Increment(ref int value) => value++;

bool TryRead(string text, out int value) =>
    int.TryParse(text, out value);

double Length(in Vector vector) =>
    Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);

4Qu'est-ce que le boxing et l'unboxing ?

Le boxing enveloppe un type valeur dans un objet de type référence en allouant un objet et en copiant la valeur à l'intérieur. L'unboxing extrait la valeur et exige le type compatible exact. Le boxing fréquent ajoute des allocations et une pression sur le ramasse-miettes (garbage collector). Les génériques évitent de nombreux cas de boxing car List<int> stocke les valeurs int sans convertir chaque valeur en object.

int number = 42;
object boxed = number;      // boxing
int restored = (int)boxed;  // unboxing

object value = 42;
// long wrong = (long)value; // InvalidCastException
long ok = (long)(int)value;

5Qu'est-ce que les types valeur nullables et les types référence nullables ?

Les types valeur nullables utilisent Nullable<T> ou T? et représentent un véritable wrapper d'exécution pour les types valeur. Les types référence nullables sont principalement une analyse compilateur : string? indique au compilateur qu'une référence peut être nulle, mais cela ne crée pas un nouveau type d'exécution. L'opérateur de non-nullité ! ne fait que supprimer les avertissements ; il ne protège pas contre NullReferenceException.

int? age = null;

string name = "Alice";
string? optionalName = null;

var length = optionalName?.Length;
var actualName = optionalName ?? "Unknown";
optionalName ??= "Default";

string trusted = optionalName!;

6Comment const, readonly et static readonly diffèrent-ils ?

const est une constante compile-time limitée aux primitives, valeurs enum, string ou null, et sa valeur est substituée dans les assemblies consommateurs pendant la compilation. readonly est un champ d'instance qui peut être assigné dans une déclaration ou un constructeur. static readonly est un champ par type, assigné dans une déclaration ou un constructeur statique. Les valeurs publiques susceptibles de changer entre les versions de bibliothèque sont plus sûres en tant que static readonly qu'en public const, car les clients peuvent conserver un ancien const inliné jusqu'à ce qu'ils soient re-compilés.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);

7Comment Equals, == et GetHashCode fonctionnent-ils ensemble ?

Pour les classes ordinaires, Equals et == comparent par défaut les références. De nombreux types intégrés et records comparent par valeur. Si Equals est surchargé, GetHashCode doit être cohérent : si a.Equals(b) est vrai, les deux objets doivent avoir le même code de hachage. L'inverse n'est pas garanti. Ne jamais muter des champs utilisés par l'égalité ou le hachage pendant que l'objet est une clé de Dictionnaire ou un élément de HashSet.

public sealed class User : IEquatable<User>
{
    public required int Id { get; init; }

    public bool Equals(User? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is User other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();
}

8Comment override diffère-t-il de new ?

override modifie le comportement virtuel, donc la méthode appelée dépend du type d'exécution de l'objet. new masque un membre de la classe de base, et le membre sélectionné dépend du type statique de la variable. Pour le polymorphisme, on veut normalement virtual et override ; masquer avec new est moins courant et peut rendre le comportement confus.

class Base
{
    public virtual void First() => Console.WriteLine("Base.First");
    public void Second() => Console.WriteLine("Base.Second");
}

class Derived : Base
{
    public override void First() => Console.WriteLine("Derived.First");
    public new void Second() => Console.WriteLine("Derived.Second");
}

Base value = new Derived();
value.First();  // Derived.First
value.Second(); // Base.Second

9Comment une classe abstraite diffère-t-elle d'une interface ?

Une classe abstraite peut contenir un état, des constructeurs, des champs, des méthodes implémentées, des méthodes abstraites et des membres protégés. Une interface décrit un contrat ; les interfaces modernes peuvent inclure des implémentations par défaut et des membres abstraits statiques, mais ce n'est toujours pas l'endroit normal pour l'état d'instance. Une classe peut hériter d'une seule classe de base mais implémenter plusieurs interfaces. Utilisez une classe abstraite pour une base commune et une implémentation partagée ; utilisez une interface pour les capacités et les limites.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}

Génériques, Collections et LINQ

10Qu'est-ce que les génériques et pourquoi sont-ils utiles ?

Les génériques vous permettent d'écrire du code sûr en termes de types et réutilisable. Ils déplacent de nombreuses vérifications au moment de la compilation, éliminent les cast manuels et peuvent éviter le boxing pour les types valeur. Les contraintes génériques décrivent ce qu'un paramètre de type doit prendre en charge, tel que l'être une classe, une structure (struct), une implémentation d'interface, une classe de base, un type non géré ou avoir un constructeur par défaut public.

public T? Find<T>(
    IEnumerable<T> items,
    Predicate<T> predicate)
{
    return items.FirstOrDefault(item => predicate(item));
}

public T Create<T>() where T : class, new()
{
    return new T();
}

11Qu'est-ce que la covariance et la contravariance ?

La covariance permet au code d'utiliser un type plus spécifique là où un type produit plus général est attendu, comme IEnumerable<string> en tant qu'IEnumerable<object>. Elle est marquée avec out sur les interfaces et les délégués. La contravariance va dans l'autre sens pour les valeurs consommées et est marquée avec in. La variance ne fonctionne que pour les interfaces et les délégués avec des types de référence ; IEnumerable<int> ne peut pas devenir IEnumerable<object> car cela nécessiterait du boxing, et List<string> n'est pas assignable à List<object>.

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // contravariance

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T value); }

12Quelle est la différence entre IEnumerable<T> et IQueryable<T> ?

IEnumerable<T> représente l'énumération .NET et les opérateurs LINQ prennent généralement des délégués tels que Func<T, bool>. IQueryable<T> stocke des arbres d'expression, par exemple Expression<Func<T, bool>>, que un fournisseur tel qu'Entity Framework Core peut traduire en SQL. Appeler ToList trop tôt matérialise les données et déplace le filtrage ultérieur en mémoire. De plus, toutes les expressions C# ne peuvent pas être traduites par un fournisseur de requêtes. Évitez de laisser fuiter IQueryable à travers toutes les couches d'application sauf si c'est un choix de conception délibéré.

IEnumerable<User> loaded = loadedUsers;
var inMemoryAdults = loaded.Where(x => x.Age >= 18);

IQueryable<User> query = dbContext.Users;
var adults = await query
    .Where(x => x.Age >= 18)
    .ToListAsync();

13Qu'est-ce que l'exécution différée (deferred execution) en LINQ ?

De nombreux opérateurs LINQ construisent une requête sans l'exécuter immédiatement. L'exécution commence lorsque la requête est énumérée ou lorsqu'une opération terminale telle que ToList, Count ou First est appelée. Cela signifie que les changements de source avant l'énumération peuvent affecter les résultats, qu'une énumération répétée peut répéter le travail ou les requêtes SQL, et que des exceptions peuvent survenir pendant l'énumération plutôt que lors de la construction de la requête.

var query = numbers.Where(x => x > 10);

numbers.Add(42);

foreach (var number in query)
{
    Console.WriteLine(number);
}

var materialized = query.ToList();

14Quelle est la différence entre array, List<T>, Dictionary<TKey,TValue> et HashSet<T> ?

Un tableau (array) a une taille fixe et un accès indexé rapide. List<T> est un tableau dynamique avec un accès indexé O(1) ; Ajouter à la fin est en amorti O(1) car l'agrandissement de la capacité alloue un plus grand tableau et copie les éléments existants, tandis que l'insertion au milieu est O(n). Dictionary<TKey,TValue> mappe des clés à des valeurs et fournit généralement une recherche par clé en O(1). HashSet<T> stocke des éléments uniques et est idéal pour les vérifications d'appartenance. Pour les collections basées sur le hachage, des implémentations correctes de Equals et GetHashCode sont essentielles.

var ids = new HashSet<int> { 1, 2, 3 };

if (ids.Contains(userId))
{
    // Fast membership check
}

15Quelle est la différence entre IEnumerable<T>, ICollection<T>, IList<T> et IReadOnlyCollection<T> ?

IEnumerable<T> ne promet que l'énumération séquentielle. ICollection<T> ajoute Count et des méthodes de mutation telles que Add et Remove. IList<T> ajoute l'accès indexé. IReadOnlyCollection<T> expose l'énumération et Count mais pas la mutation via cette interface. La conception API devrait retourner le contrat utile le plus étroit, mais les interfaces en lecture seule ne garantissent pas une immuabilité profonde de l'objet sous-jacent.

private readonly List<Order> _orders = new();

public IReadOnlyCollection<Order> Orders => _orders;

16Comment fonctionne yield return ?

yield return crée un itérateur avec exécution différée. Le compilateur transforme la méthode en machine à états (state machine), et les valeurs sont produites une par une lorsque MoveNext est appelé. Cela évite les collections intermédiaires et fonctionne bien pour les séquences volumineuses. Les pièges sont le cycle de vie et l'énumération multiple : ne retournez pas une séquence paresseuse qui dépend d'un DbContext ou d'un flux disposé, et rappelez-vous que Count suivi de foreach peut exécuter l'itérateur deux fois, répétant les effets secondaires ou les I/O.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}

Délégués, Événements et Exceptions

17Qu'est-ce qu'un délégué et des expressions lambda ?

Un délégué est une référence sûre en type à une méthode. Les lambdas fournissent des fonctions concises en ligne et sont couramment assignées aux délégués Func, Action ou Predicate. Les lambdas capturent des variables, pas des instantanés de leurs valeurs, un lambda peut donc observer la valeur d'une boucle ou d'une variable ultérieurement. Si un lambda ne capture rien, le compilateur peut mettre en cache un délégué statique et éviter l'allocation de fermeture ; dans le code sensible aux performances, les fermetures et les durées de vie d'objets étendues peuvent être importantes.

public delegate int Operation(int x, int y);

Operation operation = (x, y) => x + y;
Console.WriteLine(operation(2, 3));

int factor = 10;
Func<int, int> multiply = x => x * factor;

18En quoi un événement diffère-t-il d'un délégué ?

Un événement est généralement soutenu par un délégué, mais il restreint ce que le code externe peut faire. Le code externe peut s'abonner et se désabonner, mais seule la classe propriétaire peut déclencher l'événement. Un champ de délégué public permettrait aux appelants de remplacer la liste d'invocation ou de l'appeler directement. Les événements créent également des références du publieur à l'abonné, les publieurs de longue durée peuvent donc causer des fuites de mémoire si les abonnés ne se désabonnent pas.

public event EventHandler<OrderEventArgs>? OrderCreated;

service.OrderCreated += Handler;
service.OrderCreated -= Handler;

OrderCreated?.Invoke(this, args);

19Comment les exceptions doivent-elles être gérées en C# ?

Utilisez les exceptions pour les situations exceptionnelles, et non pour le flux de contrôle normal. Attrapez uniquement les exceptions que vous pouvez gérer, évitez les blocs catch vides, ajoutez du contexte lors de l'enveloppement et ne divulguez pas les détails internes aux utilisateurs. Utilisez throw; pour relancer tout en préservant la trace de pile originale. Si vous enveloppez une exception, transmettez l'originale comme InnerException, par exemple throw new DomainException("...", ex). Préférez les API de style TryParse pour les échecs attendus.

try
{
    await service.ProcessAsync();
}
catch (ValidationException exception)
{
    // Expected domain error
}
finally
{
    // Cleanup
}

catch
{
    throw; // preserves stack trace
}

20Qu'est-ce que les filtres d'exceptions ?

Les filtres d'exceptions permettent à un bloc catch de s'exécuter uniquement lorsqu'une condition supplémentaire est vraie. Le filtre est évalué avant le déroulement de la pile et avant d'entrer dans le corps du catch, donc lorsque il retourne false, l'original stack reste intact pour les diagnostics et les dumps de crash. Les filtres sont utiles pour les codes de statut HTTP, les codes d'erreur de base de données, la distinction entre erreurs transitoires et le journalisation conditionnelle. Gardez les filtres simples et évitez les effets secondaires complexes.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}