C# Advanced Topics


C# Advanced Topics Interview with follow-up questions

1. Can you explain what multithreading is in C# and why it's important?

Multithreading allows a program to perform multiple operations concurrently. In C#, the modern guidance is a layered approach — choose the highest abstraction that fits:

1. async/await — preferred for I/O-bound concurrency (no threads created):

public async Task FetchDataAsync(string url, CancellationToken ct)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url, ct);
}

async/await releases the calling thread while waiting; when the I/O completes, execution resumes (often on a thread pool thread). No wasted threads blocking.

2. Task.Run() — for CPU-bound work on the thread pool:

int result = await Task.Run(() => HeavyCpuComputation());

3. Parallel.For / Parallel.ForEach — data parallelism:

Parallel.ForEach(items, item => Process(item));
await Parallel.ForEachAsync(items, ct, async (item, t) => await ProcessAsync(item, t));

4. Channel (System.Threading.Channels) — async producer/consumer pipelines:

var channel = Channel.CreateBounded(capacity: 100);
// Producer
await channel.Writer.WriteAsync(work);
// Consumer
await foreach (var item in channel.Reader.ReadAllAsync())
    await HandleAsync(item);

5. Raw Thread class — rarely needed in modern code. Use for very specific scenarios (STA COM threads, etc.).

Synchronization:

Tool Use case
lock / System.Threading.Lock (.NET 9) Short synchronous critical sections
SemaphoreSlim Async-compatible throttling
Interlocked Atomic counter increments
ConcurrentDictionary Thread-safe key-value store
ReaderWriterLockSlim Many readers, few writers

Common pitfalls: race conditions on shared mutable state, deadlocks (locking in async code — avoid), async void (exceptions uncatchable — use only in event handlers).

Interviewers probe: why is async/await better than creating Thread objects for web server code? Thread-per-request doesn't scale — a 4-core server with 1,000 concurrent requests would block 1,000 threads. async/await lets thread pool threads handle many concurrent operations without blocking.

↑ Back to top

Follow-up 1

How would you handle synchronization in multithreading?

Synchronization is important in multithreading to ensure that multiple threads can safely access shared resources without causing data corruption or race conditions. In C#, synchronization can be achieved using various techniques such as locks, mutexes, semaphores, and monitors. These techniques help in controlling the access to shared resources and ensure that only one thread can access the resource at a time. Additionally, C# provides synchronization constructs like the lock statement and the Monitor class that simplify the process of synchronization.

Follow-up 2

What is the difference between a thread and a process?

A thread is a lightweight unit of execution within a process, while a process is an instance of a running program. A process can have multiple threads, each executing independently and sharing the same memory space. Threads within a process can communicate with each other more easily and efficiently compared to processes, as they share the same memory space. However, processes have their own memory space and do not share memory with other processes. Threads within a process can be scheduled and executed concurrently by the operating system, while processes are scheduled and executed independently.

Follow-up 3

Can you provide an example of when you would use multithreading in a real-world application?

One example of when multithreading can be used in a real-world application is in a web server. A web server needs to handle multiple client requests simultaneously, and using multithreading can improve its performance and responsiveness. Each client request can be handled by a separate thread, allowing the server to process multiple requests concurrently. This enables the server to serve more clients at the same time and reduces the response time for each client. Additionally, multithreading can be used in applications that perform computationally intensive tasks, such as image processing or data analysis, to take advantage of multiple CPU cores and speed up the execution.

2. What are delegates in C# and how are they used?

A delegate is a type that holds a reference to a method — a type-safe function pointer. Delegates enable passing methods as parameters, storing them in variables, and invoking them later.

Defining and using a delegate:

// Custom delegate type — rarely needed in modern C#
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 7

Built-in generic delegates (prefer these over custom types):

Type Signature Use case
Action () => void No params, no return
Action T => void One param, no return
Action (T1, T2) => void etc. (up to 16 params)
Func () => TResult No params, returns value
Func T => TResult One param, returns value
Predicate T => bool Condition test

Multicast delegates+= chains methods; -= removes them:

Action log = Console.WriteLine;
log += s => File.AppendAllText("log.txt", s + "\n");
log("Hello"); // calls both methods

Events — expose a delegate with restricted access (external code can only subscribe/unsubscribe, not invoke directly):

public class Button
{
    public event EventHandler? Clicked;
    protected virtual void OnClicked() => Clicked?.Invoke(this, EventArgs.Empty);
}

Closures — lambdas capture variables from enclosing scope:

int multiplier = 3;
Func triple = x => x * multiplier; // captures 'multiplier'

Classic closure bug in loops:

// BUG — all lambdas capture the same 'i' variable
var actions = Enumerable.Range(0, 3).Select(i => (Action)(() => Console.WriteLine(i))).ToList();
// Fix: i is already a fresh copy per iteration in C# foreach over LINQ range — but be careful with manual for loops

Interviewers probe: difference between delegate and event? An event wraps a delegate but prevents external code from invoking it directly or replacing the invocation list — only the declaring class can raise the event.

↑ Back to top

Follow-up 1

What is the difference between delegates and interfaces?

Delegates and interfaces serve different purposes in C#. Delegates are used to define and reference methods, allowing for method invocation through the delegate object. They provide a way to encapsulate a method call and are commonly used in event handling scenarios. On the other hand, interfaces define a contract that a class must implement. They specify a set of methods, properties, and events that a class must provide. Interfaces are used to achieve polymorphism and enable loose coupling between components.

Follow-up 2

Can you provide an example of a delegate in C#?

Sure! Here's an example of a delegate in C#:

public delegate int MathOperation(int a, int b);

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Subtract(int a, int b)
    {
        return a - b;
    }
}

public class Program
{
    public static void Main()
    {
        Calculator calculator = new Calculator();

        MathOperation operation = calculator.Add;
        int result = operation(5, 3);
        Console.WriteLine(result); // Output: 8

        operation = calculator.Subtract;
        result = operation(5, 3);
        Console.WriteLine(result); // Output: 2
    }
}

In this example, the MathOperation delegate is defined to represent a method that takes two int parameters and returns an int result. The Calculator class has Add and Subtract methods that match the delegate's signature. We can assign these methods to the operation delegate and invoke them through the delegate object.

Follow-up 3

How does delegate work with events in C#?

In C#, delegates are commonly used to implement event handling. An event is a mechanism for communication between objects, where an object can notify other objects when a certain action or state change occurs. Events are based on delegates, specifically the EventHandler delegate or a custom delegate type.

To work with events, you typically follow these steps:

  1. Define a delegate type that represents the signature of the event handler method.
  2. Declare an event of the delegate type in the class that will raise the event.
  3. Provide a protected virtual method to raise the event.
  4. In the class that wants to handle the event, create a method that matches the delegate's signature and subscribe to the event using the += operator.

Here's an example:

public class Button
{
    public event EventHandler Click;

    protected virtual void OnClick()
    {
        Click?.Invoke(this, EventArgs.Empty);
    }
}

public class Program
{
    public static void Main()
    {
        Button button = new Button();
        button.Click += Button_Click;

        // Simulate a button click
        button.OnClick();
    }

    private static void Button_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Button clicked!");
    }
}

In this example, the Button class declares an event named Click of type EventHandler. The OnClick method is responsible for raising the event by invoking the delegate. In the Program class, we subscribe to the Click event by providing a method (Button_Click) that matches the delegate's signature. When the OnClick method is called, it triggers the Button_Click method, which writes "Button clicked!" to the console.

3. Can you explain what LINQ is and how it's used in C#?

LINQ (Language Integrated Query) is a set of language keywords and extension methods that enable querying any data source implementing IEnumerable or IQueryable using a consistent, readable syntax.

Two syntax styles:

var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in numbers
            where n % 2 == 0
            orderby n descending
            select n * n;

// Method (fluent) syntax — more common in production code
var evens2 = numbers
    .Where(n => n % 2 == 0)
    .OrderByDescending(n => n)
    .Select(n => n * n);

Key operators:

  • Filtering: Where, OfType
  • Projection: Select, SelectMany (flatten nested collections)
  • Ordering: OrderBy, OrderByDescending, ThenBy, ThenByDescending
  • Grouping: GroupBy
  • Joining: Join, GroupJoin
  • Aggregation: Count, Sum, Average, Min, Max, Aggregate
  • Element access: First, FirstOrDefault, Single, SingleOrDefault, ElementAt
  • Set operations: Distinct, Union, Intersect, Except
  • Quantifiers: Any, All, Contains
  • Partitioning: Take, Skip, TakeWhile, SkipWhile, Chunk (.NET 6+)

.NET 6+ new methods: MinBy(), MaxBy(), DistinctBy(), ExceptBy(), IntersectBy(), UnionBy(), Chunk().

Deferred vs immediate execution:

var query = numbers.Where(n => n > 5); // deferred — not executed yet
var list = query.ToList();              // immediate — executes now

LINQ queries are lazy by default. Call ToList(), ToArray(), ToDictionary(), Count(), First(), etc. to force execution. Be aware of multiple enumeration — if you iterate a deferred query twice, it executes twice; materialize with ToList() if needed.

EF Core LINQ (IQueryable): translates LINQ to SQL. Use AsNoTracking() for read-only queries. Avoid calling .ToList() in the middle of a query chain — let EF Core translate as much as possible to SQL.

Interviewers probe: First() vs FirstOrDefault()First() throws if no element matches; FirstOrDefault() returns null/default. Use SingleOrDefault() when you expect exactly 0 or 1 results (throws on 2+).

↑ Back to top

Follow-up 1

What are the advantages of using LINQ over traditional SQL queries?

There are several advantages of using LINQ over traditional SQL queries:

  1. Integration with C#: LINQ is integrated into the C# language, which means you can write queries directly in your C# code. This eliminates the need to switch between different languages or technologies.

  2. Compile-time checking: LINQ queries are checked at compile-time, which helps catch errors early in the development process. This can save time and effort compared to traditional SQL queries, which are checked at runtime.

  3. Strongly-typed queries: LINQ queries are strongly-typed, which means the compiler can verify the correctness of the queries at compile-time. This helps prevent runtime errors and improves code reliability.

  4. Code reusability: LINQ queries can be easily reused and composed, allowing you to build complex queries by combining smaller query expressions. This promotes code reuse and reduces duplication.

  5. Query expression syntax: LINQ provides a query expression syntax that is similar to SQL, making it easier for developers who are familiar with SQL to write LINQ queries.

Follow-up 2

Can you provide an example of a LINQ query in C#?

Sure! Here's an example of a LINQ query in C#:

var numbers = new List { 1, 2, 3, 4, 5 };

var evenNumbers = from number in numbers
                  where number % 2 == 0
                  select number;

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

This query selects all the even numbers from the numbers list and prints them to the console.

Follow-up 3

What is the difference between LINQ to SQL and Entity Framework?

LINQ to SQL and Entity Framework are both Object-Relational Mapping (ORM) frameworks that allow you to work with databases using LINQ. However, there are some differences between them:

  1. Data access approach: LINQ to SQL is a lightweight ORM framework that focuses on mapping database tables to C# classes. It provides a simple and straightforward way to perform CRUD (Create, Read, Update, Delete) operations on a database. On the other hand, Entity Framework is a more feature-rich ORM framework that supports advanced mapping scenarios, complex relationships, and database-independent programming.

  2. Database support: LINQ to SQL is designed to work with Microsoft SQL Server databases. It has built-in support for SQL Server-specific features and optimizations. Entity Framework, on the other hand, is more flexible and can work with a variety of databases, including SQL Server, Oracle, MySQL, and more.

  3. Development status: LINQ to SQL is considered a legacy technology and is no longer actively developed by Microsoft. Entity Framework, on the other hand, is actively developed and is the recommended ORM framework for new projects.

Overall, if you are working with a SQL Server database and need a lightweight ORM framework, LINQ to SQL can be a good choice. If you need more advanced features, support for multiple databases, or future-proofing your application, Entity Framework is a better option.

4. What are generics in C# and why are they useful?

Generics allow you to write type-parameterized code — classes, methods, and interfaces that work with any type while preserving compile-time type safety. They eliminate the need for casting and boxing that was common with non-generic ArrayList or object-based code.

Basic usage:

// Generic method
T Max(T a, T b) where T : IComparable => a.CompareTo(b) >= 0 ? a : b;

// Generic class
public class Repository where T : class
{
    private readonly List _store = new();
    public void Add(T item) => _store.Add(item);
    public IEnumerable GetAll() => _store.AsReadOnly();
}

Generic constraints:

Constraint Meaning
where T : class Reference type
where T : struct Value type (excludes nullable)
where T : new() Has parameterless constructor
where T : SomeClass Inherits from SomeClass
where T : ISomeInterface Implements interface
where T : notnull Not nullable (C# 8+)
where T : unmanaged Unmanaged type (C# 7.3+) — for P/Invoke
where T : allows ref struct Allows ref struct (C# 13)

Variance (for generic interfaces and delegates):

  • Covariance (out T): IEnumerable can be assigned to IEnumerable because IEnumerable is covariant.
  • Contravariance (in T): Action can be assigned to Action because Action is contravariant.

C# 11 — static abstract interface members:

// Generic math — works with int, double, decimal, etc.
T Sum(IEnumerable values) where T : INumber =>
    values.Aggregate(T.Zero, (a, b) => a + b);

This is how System.Numerics.INumber works in .NET 7+.

Performance benefit: List stores int values directly on the heap without boxing. The legacy ArrayList stored everything as object, requiring boxing for value types — a hidden allocation on every Add.

Interviewers probe: what is the difference between List and List? List boxes value types and loses type safety at compile time. List is typed — no boxing for value types, compile-time type checking, no casts needed.

↑ Back to top

Follow-up 1

What is the difference between generics and collections in C#?

Generics and collections are related concepts in C#, but they serve different purposes. Generics are a language feature that allows you to define classes, interfaces, and methods that can work with different data types. They provide type safety and enable code reuse. Collections, on the other hand, are data structures that store and manipulate groups of objects. Collections can make use of generics to provide type-safe storage and retrieval of objects, but they are not limited to working with generics. In other words, generics are a language feature, while collections are a data structure concept.

Follow-up 2

Can you provide an example of a generic method in C#?

Sure! Here's an example of a generic method in C#:

public T GetMax(T a, T b) where T : IComparable
{
    return a.CompareTo(b) > 0 ? a : b;
}

In this example, the GetMax method is a generic method that takes two parameters of type T and returns the maximum value of the two. The where T : IComparable constraint ensures that the type T implements the IComparable interface, which allows the comparison of two values using the CompareTo method. This generic method can be used with different data types as long as they implement the IComparable interface.

Follow-up 3

How do generics improve type safety?

Generics improve type safety in C# by allowing you to write code that is strongly typed and avoids the need for type casting. With generics, you can specify the type of data that a class, interface, or method will work with, and the compiler enforces this type constraint. This means that you can catch type-related errors at compile-time rather than at runtime, which leads to more reliable and bug-free code. Generics also eliminate the need for boxing and unboxing operations, which can improve performance by reducing memory overhead and avoiding unnecessary type conversions.

5. Can you explain what async programming is in C# and why it's important?

Async programming in C# uses async/await to write non-blocking code that reads linearly, like synchronous code, while allowing the runtime to do other work while waiting.

Core mechanics:

public async Task FetchUserAsync(int id, CancellationToken ct = default)
{
    // Returns Task, doesn't block a thread while waiting
    var response = await _httpClient.GetAsync($"/users/{id}", ct);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(ct);
}

await suspends the method at that point, returns control to the caller (freeing the thread), and resumes when the awaited operation completes.

Return types:

Type When to use
Task Async method with no return value
Task Async method returning a value
ValueTask Hot path that often completes synchronously (avoids allocation)
void Event handlers only — avoid; exceptions are uncatchable
IAsyncEnumerable Async streams (C# 8+)

ConfigureAwait(false) — in library code, prevents capturing the synchronization context. Avoids deadlocks in some legacy environments (WinForms, ASP.NET non-Core). In ASP.NET Core, there is no synchronization context, so it's less critical but still a good practice for libraries.

CancellationToken — always accept and pass through in library code:

public async Task DoWorkAsync(CancellationToken ct)
{
    await Step1Async(ct);
    ct.ThrowIfCancellationRequested();
    await Step2Async(ct);
}

ValueTask — use instead of Task when the method frequently completes synchronously (e.g., cache hits):

public ValueTask GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
        return ValueTask.FromResult(user); // no allocation
    return new ValueTask(FetchFromDbAsync(id));
}

Common pitfalls:

  • .Result / .Wait() — blocks the thread and can cause deadlocks in UI/legacy ASP.NET contexts. Always await.
  • async void — exceptions propagate to the SynchronizationContext, not to the caller. Use Task-returning methods.
  • Fire-and-forget — not awaiting a Task means exceptions are silently swallowed. Log carefully or use structured approaches.

.NET 9 addition: Task.WhenEach() — process tasks as they complete, without waiting for all:

await foreach (var result in Task.WhenEach(task1, task2, task3))
    Console.WriteLine(await result);

Interviewers probe: I/O-bound vs CPU-bound — async/await is for I/O-bound operations (network, disk, DB). For CPU-bound work, use await Task.Run(() => HeavyComputation()) to move the work to a thread pool thread.

↑ Back to top

Follow-up 1

What is the difference between async and await in C#?

In C#, the async keyword is used to define a method that can be run asynchronously. The await keyword is used to indicate that a method call should be awaited, allowing the calling method to continue execution while the awaited method is running. When you mark a method as async, you can use the await keyword to wait for the completion of an asynchronous operation, such as a network request or a file I/O operation, without blocking the main thread.

Follow-up 2

Can you provide an example of async programming in C#?

Sure! Here's an example of an async method in C#:

public async Task GetDataAsync()
{
    HttpClient client = new HttpClient();
    string result = await client.GetStringAsync("https://api.example.com/data");
    return result;
}

In this example, the GetDataAsync method uses the HttpClient class to make an asynchronous HTTP request to retrieve data from an API. The await keyword is used to wait for the completion of the GetStringAsync method, allowing the calling method to continue execution while the request is being made. The method returns a Task object, which represents the asynchronous operation and allows the caller to await its completion.

Follow-up 3

How does async programming improve application performance?

Async programming can improve application performance by allowing your application to perform multiple tasks concurrently. When a method is marked as async and contains await statements, the method can yield control back to the calling method while waiting for an asynchronous operation to complete. This allows the calling method to continue executing other tasks, instead of being blocked and waiting for the operation to finish. By leveraging async programming, you can make your application more responsive and efficient, as it can utilize the available system resources more effectively.

6. What is the difference between `IEnumerable<T>`, `ICollection<T>`, and `IList<T>`?

These three interfaces form a hierarchy in System.Collections.Generic, each adding more capability:

Interface Capability
IEnumerable Forward-only, read-only iteration (foreach)
ICollection Adds Count, Add, Remove, Contains, Clear
IList Adds index-based access (this[int]), IndexOf, Insert, RemoveAt

When to use each in method signatures:

  • Accept IEnumerable as a parameter when you only need to iterate — this is the most flexible, accepting arrays, List, LINQ results, and any other sequence.
  • Accept ICollection when you need to add/remove or check Count.
  • Accept IList only when you need random index access.
  • Return IReadOnlyList or IReadOnlyCollection from public APIs to prevent callers from mutating your internal collection.

Concrete types: List implements all three. T[] (array) implements IList but is fixed-size (Add/Remove throw). LinkedList only implements ICollection.

Performance note: LINQ's Count() extension method checks if the source implements ICollection first and uses .Count directly (O(1)) rather than enumerating — always keep this in mind when designing collection return types.


↑ Back to top

7. What are nullable reference types in C# 8+ and why do they matter?

Before C# 8, all reference type variables could be null, and the compiler gave no warnings about potential NullReferenceException. C# 8 introduced nullable reference types as an opt-in feature.

Enabling it (in .csproj — on by default in new .NET 6+ projects):

enable

Effect:

  • string is now non-nullable by default — the compiler warns if you assign null or dereference without a null check.
  • string? explicitly declares a nullable reference type — callers know to check for null.
  • null! (null-forgiving operator) suppresses the warning when you know it's safe.
string name = null;          // Warning: cannot assign null to non-nullable
string? nickname = null;     // OK — explicitly nullable
Console.WriteLine(name.Length);    // Warning: dereference of possibly null
Console.WriteLine(nickname?.Length ?? 0); // OK — null-checked

Why it matters: NullReferenceException is one of the most common runtime exceptions. Nullable reference types move the detection from runtime to compile time, drastically reducing null bugs in large codebases. The .NET BCL itself is fully annotated.

Common gotcha — [NotNull], [MaybeNull] attributes: these flow-analysis attributes (in System.Diagnostics.CodeAnalysis) let you annotate methods whose nullability contract the compiler can't infer from the signature alone:

bool TryGet([NotNullWhen(true)] out string? value) { ... }

↑ Back to top

8. What are C# records and when should you use them?

Records (introduced in C# 9) are a class (or struct) type with built-in value-based equality, immutability support, and concise syntax.

Reference record (class-based):

public record Person(string Name, int Age);

var alice = new Person("Alice", 30);
var alice2 = new Person("Alice", 30);
Console.WriteLine(alice == alice2); // True — value equality by default

var olderAlice = alice with { Age = 31 }; // non-destructive mutation

Record struct (C# 10+, value type):

public readonly record struct Point(double X, double Y);

What the compiler generates automatically:

  • Equals() and == based on all properties
  • GetHashCode() based on all properties
  • ToString() showing property names and values
  • Positional constructor and deconstructor
  • with expression support

When to use records:

  • DTOs (data transfer objects) passed between layers
  • Value objects in domain-driven design (money amounts, coordinates)
  • Command/query models in CQRS patterns
  • Any type where two instances with the same data should be considered equal

When NOT to use records:

  • Entities with identity (a Customer is the same Customer even if their address changes — use a class with an Id)
  • Mutable objects with complex behavior

Interviewers probe: difference between record and a class with overridden Equals? Records synthesize all the equality plumbing; classes require manual implementation and it's error-prone.


↑ Back to top

9. What is dependency injection (DI) in .NET and how does the built-in container work?

Dependency injection is a design pattern where a class receives its dependencies from an external source rather than creating them itself. .NET has a built-in DI container in Microsoft.Extensions.DependencyInjection.

Registering services:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(builder.Configuration);
builder.Services.AddScoped();
builder.Services.AddTransient();

Three lifetimes:

Lifetime Created Destroyed Use for
Singleton Once per application App shutdown Stateless services, caches
Scoped Once per request (HTTP request) End of request DbContext, unit-of-work
Transient Every time requested When caller is disposed Lightweight, stateless

Captive dependency problem: injecting a Transient or Scoped service into a Singleton — the shorter-lived service gets "captured" and lives as long as the Singleton, defeating the purpose of its lifetime. .NET's built-in container throws InvalidOperationException for Scoped-in-Singleton at startup (with ValidateScopes enabled).

Constructor injection (standard pattern):

public class OrderService(IOrderRepository repo, ILogger logger)
{
    public async Task PlaceOrderAsync(Order order)
    {
        logger.LogInformation("Placing order {Id}", order.Id);
        await repo.SaveAsync(order);
    }
}

Why DI matters: testability (inject mocks), loose coupling (depend on interfaces), lifecycle management, and cross-cutting concerns (logging, caching) via decorators.


↑ Back to top

Live mock interview

Mock interview: C# Advanced Topics

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.