Vorbereitung auf C# Backend Interviews

C# Interviewfragen und Antworten 2026

Eine fokussierte Sammlung von C#-Fragen für Backend-Interviews, mit kurzen Codebeispielen für mündliche Erklärungen.

C# Mock Interview auf Deutsch starten Technische Interviews auf Englisch übenEin Modus, in dem Nicht-Muttersprachler technische Interviews üben können.

Typen und Sprachgrundlagen

1Wie unterscheiden sich Werttypen von Referenztypen?

Werttyp-Variablen enthalten normalerweise den Wert selbst, daher kopiert eine Zuweisung den Wert. Referenztyp-Variablen enthalten einen Verweis auf ein Objekt, daher kopiert eine Zuweisung den Verweis und beide Variablen können Mutationen desselben Objekts beobachten. Der wichtige Unterschied ist, dass die Speicherung vom Lebenszyklus und Kontext abhängt, nicht nur vom Typ: Ein Wert, der lokal in einer Methode existiert, kann auf dem Stack leben, während ein als Klassenfeld, Array-Element, Closure-Erfassung, verpackter Wert oder asynchroner Zustand maschine gespeicherter Werttyp mit diesem Objekt lebt.

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"

2Wie unterscheiden sich Class, Struct und Record?

Eine Klasse ist ein Referenztyp mit Objekterkennung. Ein Struct ist ein Werttyp und eignet sich am besten für kleine unveränderliche Werte wie Koordinaten, Bereiche oder Bezeichner. Eine Record-Klasse ist ein Referenztyp mit wertbasierter Gleichheit; record struct ist die Werttyp-Version. Records sind Compiler-Syntax über gewöhnlichen CLR-Typen, die Gleichheit, GetHashCode, ToString, Dekonstruktion und Kopierunterstützung generieren, einschließlich mit Ausdrücken für nicht-destruktive Mutationen. Im Backend-Code sind Records nützlich für DTO-ähnliche Modelle, während große veränderbare Structs riskant sind, da das Kopieren und Modifizieren von Kopien Leser überraschen kann.

public record User(int Id, string Name);

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

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

3Wie unterscheiden sich ref, out und in Parameter?

ref übergibt ein Argument per Referenz und erfordert, dass es vor dem Aufruf initialisiert ist; die Methode kann es lesen und ändern. out gibt ebenfalls per Referenz weiter, aber die Methode muss einen Wert zuweisen, bevor sie zurückgibt. in gibt ein lesbares byref weiter, was das Kopieren für große Structs vermeiden kann, aber es ist keine tiefgehende Unveränderlichkeitsgarantie: veränderbare Structs und nicht-lesbare Mitglieder können defensive Kopien erzwingen. Auf der IL-Ebene werden ref, out und in als managed byref Parameter mit unterschiedlichen C#-Regeln verwaltet.

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);

4Was sind Boxing und Unboxing?

Boxing verpackt einen Werttyp in ein referenztypsbasiertes Objekt, indem es ein Objekt zuweist und den Wert hineinkopiert. Unboxing extrahiert den Wert zurück und erfordert den exakt kompatiblen Typ. Häufiges Boxing fügt Speicherzuweisungen hinzu und erhöht den Druck auf den Garbage Collector. Generics vermeiden viele Boxing-Fälle, da List<int> int-Werte speichert, ohne jeden Wert in ein Objekt zu konvertieren.

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;

5Was sind nullable Werttypen und nullable Referenztypen?

Nullable Werttypen verwenden Nullable<T> oder T? und stellen einen echten Laufzeitwrapper für Werttypen dar. Nullable Referenztypen sind größtenteils Compiler-Analysen: string? sagt dem Compiler, dass ein Verweis null sein kann, erzeugt aber keinen neuen Laufzeittyp. Der null-forgiving Operator ! unterdrückt nur Warnungen; er schützt nicht vor NullReferenceException.

int? age = null;

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

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

string trusted = optionalName!;

6Wie unterscheiden sich const, readonly und static readonly?

const ist eine Kompilierzeitkonstante, die auf Primitiven, Enum-Werten, Strings oder null beschränkt ist, und ihr Wert wird während der Kompilierung in konsumierende Baugruppen substituiert. readonly ist ein Instanzfeld, das in einer Deklaration oder einem Konstruktor zugewiesen werden kann. static readonly ist ein Feld pro Typ, das in einer Deklaration oder einem statischen Konstruktor zugewiesen wird. Öffentliche Werte, die über Bibliotheksversionen hinweg geändert werden könnten, sind als static readonly sicherer als als public const, da Clients einen alten eingebetteten const behalten können, bis sie neu kompiliert werden.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

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

7Wie arbeiten Equals, == und GetHashCode zusammen?

Für gewöhnliche Klassen vergleichen Equals und == standardmäßig Verweise. Viele eingebaute Typen und Records vergleichen nach Wert. Wenn Equals überschrieben wird, muss GetHashCode konsistent sein: wenn a.Equals(b) wahr ist, müssen beide Objekte denselben Hashcode haben. Das Gegenteil ist nicht garantiert. Mutieren Sie niemals Felder, die für Gleichheit oder Hashing verwendet werden, während das Objekt ein Schlüssel in einem Dictionary oder ein Element in einem HashSet ist.

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();
}

8Wie unterscheidet sich override von new?

override ändert das virtuelle Verhalten, sodass die aufgerufene Methode vom Laufzeittyp des Objekts abhängt. new versteckt ein Mitglied aus der Basisklasse, und das ausgewählte Mitglied hängt vom statischen Typ der Variable ab. Für Polymorphie möchte man normalerweise virtual und override; Verbergen mit new ist weniger üblich und kann das Verhalten verwirrend machen.

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

9Wie unterscheidet sich eine abstrakte Klasse von einer Schnittstelle?

Eine abstrakte Klasse kann Zustand, Konstruktoren, Felder, implementierte Methoden, abstrakte Methoden und geschützte Mitglieder enthalten. Eine Schnittstelle beschreibt einen Vertrag; moderne Schnittstellen können Standardimplementierungen und statische abstrakte Mitglieder enthalten, sind aber immer noch nicht der normale Ort für Instanzzustände. Eine Klasse kann von einer Basisklasse erben, aber mehrere Schnittstellen implementieren. Verwenden Sie eine abstrakte Klasse für eine gemeinsame Basis und Implementierung; verwenden Sie eine Schnittstelle für Fähigkeiten und Grenzen.

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

Generics, Collections und LINQ

10Was sind Generics und wofür sind sie nützlich?

Generics ermöglichen es Ihnen, wiederverwendeten, typsicheren Code zu schreiben. Sie verlagern viele Prüfungen in die Kompilierzeit, eliminieren manuelle Casts und können das Boxing für Wertetypen vermeiden. Generische Einschränkungen beschreiben, was ein Typparameter unterstützen muss, wie z. B. eine Klasse, eine Struktur, eine Schnittstellenimplementierung, eine Basisklasse, einen unverwalteten Typ oder das Vorhandensein eines öffentlichen parameterlosen Konstruktors.

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();
}

11Was sind Kovarianz und Kontravarianz?

Kovarianz ermöglicht es dem Code, einen spezifischeren Typ zu verwenden, wo ein allgemeinere erzeugter Typ erwartet wird, wie z. B. IEnumerable<string> als IEnumerable<object>. Sie ist an Schnittstellen und Delegaten mit out gekennzeichnet. Kontravarianz geht in die andere Richtung für konsumierte Werte und ist mit in gekennzeichnet. Varianz funktioniert nur für Schnittstellen und Delegaten mit Referenztypen; IEnumerable<int> kann nicht zu IEnumerable<object> werden, da dies Boxing erfordern würde, und List<string> ist nicht zu List<object> zugewiesen.

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); }

12Wie unterscheidet sich IEnumerable<T> von IQueryable<T>?

IEnumerable<T> repräsentiert die .NET-Enumeration, und LINQ-Operatoren verwenden normalerweise Delegaten wie Func<T, bool>. IQueryable<T> speichert Ausdrucksbäume (Expression Trees), zum Beispiel Expression<Func<T, bool>>, die ein Provider wie Entity Framework Core in SQL übersetzen kann. Das frühe Aufrufen von ToList materialisiert Daten und verlagert spätere Filterung in den Speicher. Außerdem kann nicht jeder C#-Ausdruck von einem Query-Provider übersetzt werden. Vermeiden Sie es, IQueryable durch alle Anwendungsschichten zu „lecken“, es sei denn, dies ist eine bewusste Designentscheidung.

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();

13Was ist deferred execution (verzögerte Ausführung) in LINQ?

Viele LINQ-Operatoren bauen eine Abfrage auf, ohne sie sofort auszuführen. Die Ausführung beginnt, wenn die Abfrage enumeriert oder wenn ein terminaler Operator wie ToList, Count oder First aufgerufen wird. Das bedeutet, dass sich Quellenänderungen vor der Enumeration auswirken können, wiederholte Enumerationen Arbeit oder SQL-Abfragen wiederholen und dass Ausnahmen während der Enumeration und nicht während des Query-Aufbaus auftreten können.

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

numbers.Add(42);

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

var materialized = query.ToList();

14Wie unterscheiden sich Array, List<T>, Dictionary<TKey,TValue> und HashSet<T>?

Ein Array hat eine feste Größe und einen schnellen indexbasierten Zugriff. List<T> ist ein dynamisches Array mit O(1) indexbasiertem Zugriff; das Hinzufügen am Ende ist amortisiert O(1), da die Vergrößerung der Kapazität ein größeres Array zuweist und vorhandene Elemente kopiert, während eine Einfügung in der Mitte O(n) ist. Dictionary<TKey,TValue> bildet Schlüssel Werten ab und bietet normalerweise einen O(1)-Schlüsselabruf. HashSet<T> speichert eindeutige Elemente und ist ideal für Mitgliedschaftsprüfungen. Für hashbasierte Sammlungen sind korrekte Implementierungen von Equals und GetHashCode unerlässlich.

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

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

15Wie unterscheiden sich IEnumerable<T>, ICollection<T>, IList<T> und IReadOnlyCollection<T>?

IEnumerable<T> verspricht nur eine sequentielle Enumeration. ICollection<T> fügt Count und Mutationsmethoden wie Add und Remove hinzu. IList<T> fügt den indexbasierten Zugriff hinzu. IReadOnlyCollection<T> legt die Enumeration und Count frei, aber keine Mutation über diese Schnittstelle. Das API-Design sollte den engsten nützlichen Vertrag zurückgeben, aber lesemodus-Schnittstellen garantieren keine tiefe Unveränderlichkeit des zugrunde liegenden Objekts.

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

public IReadOnlyCollection<Order> Orders => _orders;

16Wie funktioniert yield return?

yield return erstellt einen Iterator mit verzögerter Ausführung. Der Compiler wandelt die Methode in eine Zustandsmaschine um, und Werte werden einzeln produziert, wenn MoveNext aufgerufen wird. Dies vermeidet Zwischensammlungen und eignet sich gut für große Sequenzen. Die Fallstricke sind die Lebensdauer und die mehrfache Enumeration: geben Sie keine faule Sequenz zurück, die von einem disposed DbContext oder Stream abhängt, und denken Sie daran, dass Count gefolgt von foreach den Iterator zweimal ausführen kann, wodurch Seiteneffekte oder I/O wiederholt werden.

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

Delegaten, Events und Ausnahmen

17Was sind Delegaten und Lambda-Ausdrücke?

Ein Delegate ist eine typsichere Referenz auf eine Methode. Lambdas bieten prägnante Inline-Funktionen und werden häufig Func-, Action- oder Predicate-Delegaten zugewiesen. Lambdas erfassen Variablen, nicht Snapshots ihrer Werte, sodass ein Lambda einen späteren Schleifen- oder Variablenwert beobachten kann. Wenn ein Lambda nichts erfasst, kann der Compiler einen statischen Delegate zwischenspeichern und die Allokation eines Closures vermeiden; in leistungskritischem Code können Closures und verlängerte Objektlebensdauern relevant sein.

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;

18Wie unterscheidet sich Event von Delegate?

Ein Event wird normalerweise durch einen Delegate unterstützt, aber es schränkt ein, was externer Code tun kann. Externer Code kann abonnieren und abbestellen, aber nur die Besitzerklasse kann das Event auslösen (raise). Ein öffentliches Delegate-Feld würde es Aufrufern ermöglichen, die Aufrufliste zu ersetzen oder es direkt aufzurufen. Events erstellen auch Referenzen vom Publisher zum Subscriber, daher können langlebige Publisher Speicherlecks verursachen, wenn Abonnenten nicht abbestellen.

public event EventHandler<OrderEventArgs>? OrderCreated;

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

OrderCreated?.Invoke(this, args);

19Wie sollten Ausnahmen in C# behandelt werden?

Verwenden Sie Ausnahmen für außergewöhnliche Situationen, nicht für normalen Kontrollfluss. Fangen Sie nur Ausnahmen ab, die Sie behandeln können, vermeiden Sie leere Catch-Blöcke, fügen Sie Kontext hinzu beim Einpacken (wrapping) und geben Sie interne Details nicht an Benutzer weiter. Verwenden Sie throw;, um erneut auszuwerfen und dabei die ursprüngliche Stack Trace zu bewahren. Wenn Sie eine Ausnahme einpacken, übergeben Sie das Original als InnerException, z. B. throw new DomainException("String", ex). Bevorzugen Sie APIs im TryParse-Stil für erwartete Fehler.

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

catch
{
    throw; // preserves stack trace
}

20Was sind Exception Filters?

Exception Filters lassen einen Catch-Block nur ausführen, wenn eine zusätzliche Bedingung wahr ist. Der Filter wird vor dem Stack Unwinding und vor dem Betreten des Catch-Blocks ausgewertet, sodass er bei false den ursprünglichen Stack für Diagnosen und Crash Dumps intakt lässt. Filter sind nützlich für HTTP Status Codes, Datenbankfehlercodes, zur Unterscheidung von transienten Fehlern und bedingter Protokollierung. Halten Sie die Filter einfach und vermeiden Sie komplexe Nebenwirkungen.

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