78 Essential .NET Interview Questions *

Toptal sourced essential questions that the best .NET developers can answer. Driven from our community, we encourage experts to submit questions and offer feedback.

Hire a Top .NET Developer Now
Toptal logois an exclusive network of the top freelance software developers, designers, marketing experts, product managers, project managers, and management consultants in the world. Top companies hire Toptal freelancers for their most important projects.
Julio Szabo

Julio is a senior software engineer with over 13 years of experience developing and building scalable web applications using Microsoft .NET. He delivers high-quality end-to-end solutions across complex systems, using C#, ASP.NET, SQL Server, and microservices architectures, while leveraging cloud platforms, CI/CD, and Agile practices to ensure reliable, scalable delivery. With a strong background in full-stack development, system design, and team leadership, Julio provides real-world insights into how top .NET developers think and perform in practical applications.

This list of .NET interview questions and answers was curated by experienced .NET developers from Toptal’s global talent network, which accepts only the top 3% of applicants.

These .NET questions cover core runtime concepts, C# features, architecture, and modern ASP.NET Core patterns. They are suited to applicants at all experience levels. Whether you’re screening .NET developers or preparing for a .NET interview, these questions will assess readiness through core .NET practical knowledge and problem-solving skills.
1.

What are the differences between REST, GraphQL, and gRPC? When would you choose each?

View answer

REST:

  • Uses fixed endpoints that return JSON responses. Simple, with universal browser support, and easy to debug.
  • There is a risk of over-fetching or under-fetching data as the server determines the response structure.
  • Choose for public APIs.

GraphQL:

  • Uses a single endpoint where the client specifies exactly which fields it needs.
  • Eliminates over-fetching and under-fetching.
  • Is best suited for complex frontends, such as mobile apps and single-page applications (SPAs).
  • The trade-off is added server complexity, which makes caching more difficult.
  • Choose for flexible client queries.

gRPC:

  • Uses a binary protocol called Protocol Buffers over HTTP/2.
  • Extremely fast and efficient; service contracts are code-generated from .proto files.
  • Ideal for internal service-to-service communication.
  • The trade-offs are poor browser support and that it’s not human-readable.
  • Choose for internal microservice calls.

gRPC contract example (.proto):

rpc GetOrder (OrderRequest) returns (OrderReply);

Why this question matters:

This is an API design .NET interview question that tests whether mid-to-senior-level candidates can make informed decisions and choose the appropriate communication patterns based on real-world system requirements. Hiring managers should listen for candidates to clearly explain the trade-offs between REST, GraphQL, and gRPC, rather than describing them individually. Strong candidates will discuss when simplicity and broad compatibility matter, when flexible data querying is beneficial, and when performance is critical to internal services.

2.

What is the difference between concurrency and parallelism in .NET?

View answer

Concurrency: Multiple tasks are in progress at the same time, but not necessarily running simultaneously. A single thread switches between tasks cooperatively. In .NET, async/await is the primary concurrency model. The thread is freed up during I/O-bound operations and picks up other work while waiting.


await Task.WhenAll(FetchUsersAsync(), FetchOrdersAsync());

Parallelism: Multiple tasks execute simultaneously on multiple CPU cores. In .NET, Parallel.ForEachAsync and Task.WhenAll leverage the thread pool to achieve true parallel execution.

            
Parallel.ForEach(items, item => HeavyComputation(item));

Rule: Use concurrency (async/await) for I/O-bound work; use parallelism (‘Parallel’, ‘PLINQ’) for CPU-bound work. Mixing both without care can lead to thread starvation.

3.

Explain the role of GC Generations and the Large Object Heap (LOH). How do you reduce GC pressure in a high-throughput .NET service?

View answer

The .NET Garbage Collector (GC) uses a generation model that significantly boosts performance by collecting short-lived data more frequently than long-lived data:

  • Gen 0 contains short-lived objects and is collected most often.
  • Gen 1 contains objects that survive Gen 0 collection and acts as a buffer tier.
  • Gen 2 contains long-lived objects such as caches and statics; a full GC here is expensive.
  • LOH contains objects larger than 85 KB. It’s prone to fragmentation and is only collected during Gen 2 collection.

How to reduce GC pressure:

  • Use ArrayPool<T> or MemoryPool<T> to reuse buffers.
  • Prefer Span<T> or Memory<T> for stack-based slices.
  • Avoid boxing, use generics instead.
  • Profile with dotnet-gcdump or PerfView.

var buf = ArrayPool<byte>.Shared.Rent(4096);
try
{
    /* use buf */
}
finally
{
    ArrayPool<byte>.Shared.Return(buf);
}

Why this question matters:

This performance and memory management .NET interview question tests whether senior-level candidates understand how .NET applications behave under load. Hiring managers should expect candidates to explain how the generational GC model works and why large object allocations can create performance issues and bottlenecks. Adept candidates will describe practical techniques like buffer reuse, minimizing allocations, and using profiling tools to identify pressure points.

4.

What is the difference between value types and reference types in the CLR? How does it affect memory layout and performance?

View answer

Value types (such as struct, int, and bool):

  • Stored inline, either on the stack or inside the containing object.
  • Copying a value duplicates all data and requires no GC tracking overhead.

Reference types (such as class, and string):

  • Stored on the managed heap.
  • Copying the variable only copies a pointer (a reference to the object).
  • Every allocation increases GC pressure.

Boxing converts a value type to object, which requires a heap allocation:


int x = 42;
object boxed = x;   
int y = (int)boxed; 

Rule: Use structs for small (less than 16 bytes), immutable, frequently allocated data.

Why this question matters:

This .NET core interview question is a foundational runtime question testing whether junior-to-mid-level candidates understand how data is stored and managed in memory. Hiring managers should watch for candidates who can explain how value and reference types differ in allocation, behavior, and garbage collection impact. Standout candidates will connect these differences to their real-world impact on performance, memory usage, or unintended side effects like boxing.

5.

How does async/await work under the hood? What is the compiler-generated state machine?

View answer

The C# compiler rewrites each async method into a structure that implements IAsyncStateMachine.

When execution hits an await:

  1. The current local variables and execution position are saved as struct fields in the state machine.
  2. A continuation is scheduled on the awaited Task.
  3. The current thread is returned to the thread pool immediately.
  4. When the Task completes, MoveNext() resumes execution at the saved state.

Key insight: async/await does not add threads, it enables cooperative multitasking. The continuation runs on whichever thread the Task completes on, unless a SynchronizationContext is present.

Why this question matters:

This asynchronous programming .NET interview question distinguishes mid-to-senior-level candidates who have only used async/await from applicants with a true understanding of how async execution works. Hiring managers should look for those who can explain the role of the compiler-generated state machine and how execution is paused and resumed without blocking threads. Strong candidates understand that async/await enables cooperative multitasking rather than creating new threads for efficient, non-blocking code execution.

6.

What are the differences between Task<T>, ValueTask<T>, and IAsyncEnumerable<T>? When is ValueTask beneficial?

View answer

Task<T>: Always allocates a heap object and should be used as the default async return type.

ValueTask<T>: A struct that wraps either a result or a Task. Use when the result is often already available (such as from a cache hit) to avoid a heap allocation.

IAsyncEnumerable<T>: Represents asynchronous data streams consumed with await foreach. Ideal for paginated APIs, database cursors, and real-time feeds.


public async ValueTask<User> GetUserAsync(int id) {
    if (_cache.TryGetValue(id, out var u)) return u;
    return await _db.FindAsync(id);
}

Note: Never await a ValueTask more than once.

7.

What tools and techniques would you use to diagnose a memory leak in a .NET application?

View answer

Common causes of memory leaks include event handlers that are never unsubscribed, static collections, async closures capturing large objects, and undisposed IDisposables instances.

Diagnostic workflow:

  1. Capture two heap snapshots using dotnet-gcdump, one from before the suspected leak and one after.
  2. Compare surviving object counts to identify which types are growing.
  3. Use dotnet-dump analyze to trace GC roots and determine what is keeping the objects alive.
  4. In complex cases, use PerfView or JetBrains dotMemory for visual comparison and analysis.

Additional tools include dotnet-trace, Visual Studio Diagnostic Tools, and Application Insights Live Metrics.

Why this question matters:

This senior-level .NET developer interview question about production diagnostics measures a candidates real-world debugging abilities. Hiring managers should expect candidates to describe a structured approach for investigating leaks and diagnosing production memory issues using the right tools and root-cause analysis techniques. Experienced developers will reference tools such as dotnet-gcdump, dotnet-dump`, or profiling tools, and explain how they use them in real-world production environments.

8.

How do C# records differ from classes? What happens if you override Equals or GetHashCode on a record?

View answer

record types (introduced in C# 9+) are reference types with compiler-generated value semantics:

  • Equals and GetHashCode are automatically generated based on all properties.
  • Non-destructive mutation via with expressions.
  • Positional deconstruction is supported for records defined with parameters.

public record Point(int X, int Y);

var p2 = new Point(1, 2) with { X = 10 };

Console.WriteLine(new Point(1,2) == new Point(1,2));

Overriding Equals or GetHashCode replaces the compiler-generated behavior. During overrides, always call base.Equals() and base.GetHashCode() to preserve value semantics. Otherwise equality falls back to reference comparison, breaking the expected behavior of a record contract.

9.

What are C# Source Generators and how do they improve performance or maintainability?

View answer

C# Source Generators run during compilation via the Roslyn compiler API and inject new C# source code into the build.

Benefits:

  • Eliminate runtime reflection, resulting in a faster startup and AOT compatible compilation.
  • Catch errors at compile time instead of runtime.
  • Generate boilerplate code, such as serialization, logging, or mapping, without relying on magic strings.

Real-world examples:

  • Zero-allocation structured logging (compile-time).

[LoggerMessage(Level=LogLevel.Information, Message="Order {Id} placed")]
partial void LogOrder(int id);

  • STJ source gen, no reflection at runtime.

[JsonSerializable(typeof(Order))]
partial class AppJsonCtx : JsonSerializerContext { }

10.

What is the difference between var and dynamic in C#?

View answer

var: Compile-time type inference.

  • The type is determined by the compiler, so the variable is fully statically typed.
  • IntelliSense and normal type checking work normally, with no runtime overhead.

Example:


var x = 42;

dynamic: Runtime type resolution via the DLR (Dynamic Language Runtime).

  • The compiler skips type checking and binding happens at runtime.
  • Typically used only for COM interop or other late-bound scenarios.

Example:


dynamic d = 42;
d.NoSuchMethod();

Rule: Prefer var whenever possible and use dynamic only when the type genuinely cannot be determined at compile time.

11.

Explain DI service lifetimes (Singleton, Scoped, Transient) in ASP.NET Core and what bugs arise from mixing them incorrectly.

View answer

Singleton: A single instance is created for the entire lifetime of the application.

Scoped: One instance is created per HTTP request.

Transient: A new instance is created for every injection.

The Captive Dependency anti-pattern is a common issue, where a Singleton depends on a Scoped service. This causes a stale, request-specific instance to be held beyond its intended lifetime, leading to unexpected behaviors.

Example of a bug where the Singleton service captures a scoped DbContext:


services.AddSingleton<MySvc>(); 
services.AddScoped<AppDbContext>();

The fix for this bug is to inject IServiceScopeFactory and create a scope manually.


public MySvc(IServiceScopeFactory f) => _f = f;

void DoWork()
{
    using var s = _f.CreateScope();
    var db = s.ServiceProvider.GetRequiredService<AppDbContext>();
}

Why this question matters:

This mid-level practical architecture .NET interview question evaluates whether a candidate understands how dependency lifetimes behave within an ASP.NET Core application. Hiring managers should listen for clear explanations of each service lifetime and the issues that arise if they are misused. Applicants should highlight their practical experience designing reliable systems that avoid subtle, difficult-to-trace production bugs.

12.

When should you use the Factory pattern versus the Strategy pattern in .NET?

View answer

Factory is used when object creation varies, based on context:

  • Centralizes and abstracts construction logic.
  • The caller is decoupled from the concrete implementation.

IPaymentHandler h = PaymentHandlerFactory.Create(config.Provider);

Strategy is used when behavior varies but object creation stays the same:

  • Encapsulates interchangeable algorithms behind a common interface.

IDiscountStrategy s = isBF ? new BFDiscount() : new StandardDiscount();
decimal total = s.Apply(cart);

A common approach is to combine the patterns and use a Factory to create the correct Strategy. This is often seen in plugin architectures and keyed Dependency Injection (DI) registrations.

13.

What is Dependency Injection (DI) in .NET and why is it important?

View answer

Dependency Injection (DI) is a design pattern in which a class receives dependencies from outside, rather than creating them internally.

An example without DI, tightly coupled, untestable:


public class OrderSvc
{
    private EmailSender _s = new EmailSender();
}

An example with DI, loosely coupled, testable:


public class OrderSvc
{
    public OrderSvc(IEmailSender s) => _sender = s;
    private readonly IEmailSender _sender;
}

Registration in Program.cs:


builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

Benefits include improved testability (injecting mocks), greater flexibility (swapping implementations), and better object lifetime management.

14.

What is Native AOT in modern .NET and why does it matter for cloud-native applications?

View answer

Traditional .NET uses JIT compilation at startup. Native AOT compiles the entire application into native machine code at build time.

Advantages for cloud-native workloads:

  • Can deliver sub-10ms cold starts, which are critical for serverless environments such as Lambda or ACA.
  • Can produce binaries that are 50–80% smaller after framework trimming.
  • Reduces the memory footprint, which allows for higher container density.

Example project setting:


<!-- In .csproj →

<PublishAot>true</PublishAot>

Trade-offs include no runtime reflection (Source Generators are typically used instead), longer build times, and incompatibility with some NuGet packages.

15.

What is the difference between Middleware and Filters in ASP.NET Core?

View answer

Middleware operates at the HTTP pipeline level. It runs for every request before routing and has no knowledge of controllers or actions. It’s typically used for concerns such as authentication, CORS, logging, and exception handling.

Filters run inside the MVC pipeline and only apply when a controller action is matched. They have access to action context and model state. They are typically used for concerns such as model validation, result shaping, and action auditing.

Middleware example:


app.Use(async (ctx, next) =>
{
    await next();
});

Action Filter example:

public class ValidateModel : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext ctx)
    {
        if (!ctx.ModelState.IsValid)
            ctx.Result = new BadRequestObjectResult(ctx.ModelState);
    }
    public void OnActionExecuted(ActionExecutedContext ctx)
    {
    }
}

16.

What are design patterns and why should a .NET developer understand them?

View answer

Design patterns are proven, reusable solutions to common software design problems. They describe intent and structure rather than specific code implementations.

ASP.NET Core is pattern-heavy by design:

  • Middleware pipeline, Chain of Responsibility
  • Built-in DI container, Factory and Service Locator
  • ILogger with providers, Observer and Strategy
  • IOptions<T>, Options pattern

Example Observer pattern using C# events as the built-in implementation:


public class OrderSvc
{
    public event EventHandler<Order> OrderPlaced;

    public void Place(Order o)
    {
        OrderPlaced?.Invoke(this, o);
    }
}

Recognizing patterns helps a .NET developer read framework code, communicate design decisions clearly, and avoid reinventing solutions that .NET already provides.

17.

What problem does Dependency Injection solve, and what new problems does it introduce?

View answer

Dependency Injection (DI) solves tight coupling by allowing classes to receive their dependencies from the outside, which makes code more testable and flexible.

Problems that DI can introduce:

  • Indirection. It can be harder to trace which concrete implementation is running without inspecting the composition root.
  • Captive dependencies. Injecting a Scoped service into a Singleton can cause stale or incorrect state.
  • Over-injection. (God classes) Constructors with 8+ parameters can hide design problems and make classes harder to understand.
  • Misconfiguration. Using incorrect lifetimes (AddScoped vs AddSingleton) leads to runtime errors, not compile-time.

Healthy practice: Use DI for infrastructure boundaries such as database access, HTTP clients, or logging. Keep core domain logic as pure, dependency-free objects.

18.

What is .NET Aspire and when should you use or avoid it?

View answer

.NET Aspire is an opinionated stack for building observable, cloud-ready distributed applications.

It includes:

  • AppHost. Orchestrates multiple services, such as APIs, workers, Redis, and SQL, for local development.
  • Service defaults. Preconfigured support OpenTelemetry, health checks, and resilience.
  • Dashboard. Real-time logs, traces, and metrics during local development.

Example:


var b = DistributedApplication.CreateBuilder(args);
var redis = b.AddRedis("cache");

b.AddProject<Projects.MyApi>("api")
    .WithReference(redis);

b.Build().Run();

Use it when working with multiple services and infrastructure dependencies, especially when you need local production parity.

Avoid it for single-service applications where it introduces unnecessary complexity. It’s not a Kubernetes replacement.

19.

Using EF Core, how would you automatically filter soft-deleted entities and apply a per-organization filter globally? How do you selectively disable one filter?

View answer

Use EF Core global query filters, which are applied automatically to every query for a given entity type.

Configure the filter in OnModelCreating:


modelBuilder.Entity<Invoice>()
    .HasQueryFilter(i => !i.IsDeleted && i.OrgId == _currentOrgId);

EF Core 10 introduces named global query filters, which allow selective removal:


modelBuilder.Entity<Invoice>()
    .HasQueryFilter("softDelete", i => !i.IsDeleted)
    .HasQueryFilter("orgScope", i => i.OrgId == _currentOrgId);

Disable only the soft-delete filter while keeping the organization filter active:


var results = await db.Invoices
    .IgnoreQueryFilters(["softDelete"])
    .ToListAsync();

Before EF Core 10, IgnoreQueryFilters() removed all filters and any required filters had to be manually reapplied.

20.

Explain the difference between IEnumerable<T> and IQueryable<T> in C#. When would you use each one and what are the performance implications of choosing the wrong abstraction when working with a database through Entity Framework?

View answer

Core difference:

IEnumerable<T> executes queries in-memory (client-side) using LINQ to Objects.

IQueryable<T> executes queries against the database (server-side) by translating LINQ expressions into SQL.

Performance impact:

Incorrect usage example using IEnumerable<T> with Entity Framework. Loads all users, then filters in memory.


IEnumerable<User> users = dbContext.Users; 
var active = users.Where(u => u.IsActive); 

Correct usage example using IQueryable<T> with Entity Framework. Translates to SQL WHERE clause.


IQueryable<User> users = dbContext.Users;
var active = users.Where(u => u.IsActive).ToList(); 

When to use:

Use IQueryable<T> for database queries where filtering and sorting should be executed at the database level.

Use IEnumerable<T> for in-memory collections where the data has already been loaded.

Key risk:

Using IEnumerable<T> with a database causes entire tables to be loaded into memory before filtering, leading to major performance issues with large datasets.

Why this question matters:

This is a data access and performance .NET interview question for mid-to-senior-level candidates, testing their understanding of how queries are executed in .NET applications. Hiring managers should watch for candidates who can clearly explain the difference between in-memory execution and database-side query translation and discuss the performance implications. Standout applicants will know how to work effectively with Entity Framework to prevent costly performance issues in production systems.

21.

How would you design a resilient ASP.NET Core API that handles downstream failures without cascading?

View answer

Resilience should be applied in layers, with each layer addressing a different failure mode.

Here`s a structured strategy for failures that are contained, limited, and observable:

  1. Timeouts and CancellationToken. Cap latency on every I/O call.
  2. Retry with exponential backoff. Only for idempotent operations.
  3. Circuit Breaker (Polly). Fail fast when a dependency is repeatedly unavailable.
  4. Bulkhead. Limit concurrency per dependency to protect the thread pool.
  5. Fallback or graceful degradation. Return cached data or default responses when appropriate.
  6. Health checks. Such as /health/ready and /health/live for Kubernetes probes.
  7. OpenTelemetry traces and structured logs. To detect cascade patterns early.

Example of configuring standard resilience for an HTTP client using the newer Polly v8 Microsoft resilience system to configure common resilience features in one line:


builder.Services.AddHttpClient<IWeatherClient, WeatherClient>()
    .AddStandardResilienceHandler();

Why this question matters:

This senior-level .NET interview question evaluates system design thinking and tests whether a candidate knows how to build reliable APIs in distributed environments. Hiring managers should look for candidates who can describe layered resilience strategies rather than a single solution. Experienced developers will discuss how they design fault-tolerant systems that maintain stability and prevent small failures from impacting the entire application.

22.

What are the key architectural strategies for scaling a .NET application?

View answer

Key architectural strategies for scaling a .NET application include:

  1. Stateless services. Avoid in-process session state so any instance can handle any request. Store state in Redis or a database instead.
  2. Async I/O throughout. async/await frees threads while waiting on I/O, directly improving throughput.
  3. Caching. IMemoryCache is useful for single-node hot data, while IDistributedCache (like Redis) is better for multi-node environments.
  4. Database optimization. Use indexes, read replicas, and CQRS to separate read and write paths.
  5. Horizontal scale. Deploy behind a load balancer with /ready health-check endpoints.
  6. Background work. Offload long-running tasks to queues such as Service Bus, or RabbitMQ with IHostedService workers.
  7. Observability. Structured logs and distributed traces are mandatory. Without metrics it`s difficult to identify bottlenecks.

Why this question matters:

This is an architecture .NET interview question for senior-level candidates, evaluating their ability to design for scalable growth and performance at the system level. Hiring managers should listen for developers who discuss practical application decisions and strategies, such as stateless services and asynchronous processing. Strong applicants should know how these approaches work together to improve throughput and achieve reliability at scale.

23.

How do you approach microservices design with .NET?

View answer

Core principles:

  • Single responsibility. Each service owns one business capability and its own data store.
  • Autonomous deployment. Each service has its own separate CI/CD pipeline and can be deployed independently.
  • API contract. Use REST or gRPC, with explicit versioning.

Communication:

  • Synchronous. REST or gRPC for low-latency request-response scenarios.
  • Asynchronous. Use a message broker, like Service Bus or RabbitMQ, for commands and events to decouple services and improve resilience.

Operational:

  • OpenTelemetry and correlation IDs. F or distributed tracing.
  • Health checks. Such as /live and /ready, for Kubernetes probes.
  • Centralized structured logging. For example Seq, Elastic, or Azure Monitor.

When NOT to use microservices:

  • For small teams of less than five engineers.
  • When there are unclear domain boundaries.
  • If there are strong requirements for strict data consistency.
24.

What is a CancellationToken in .NET and how should it be used?

View answer

A CancellationToken is .NET`s standard mechanism for cooperative cancellation of asynchronous or long-running operations.

Create with a timeout (cancels after a set time):


using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

Pass the token through the entire call chain (to cancel downstream operations):


await _repo.GetDataAsync(cts.Token);

Inside the method honors the token (stops work if cancellation is requested) either explicitly through ThrowIfCancellationRequested() or by passing the token to async APIs:


ct.ThrowIfCancellationRequested();
await db.SaveChangesAsync(ct);

ASP.NET Core automatically provides a CancellationToken to controller action and injects from the action signature for free cancellation on client disconnect:


public async Task<IActionResult> Get(CancellationToken ct)
{
}

Always propagate the CancellationToken through the entire call chain. Ignoring it means the operation consumes resources even after the client disconnects.

25.

What happens when you use ‘async/await and why can improper usage cause deadlocks?

View answer

Using async/await does not create a new thread. Instead it releases the calling thread while waiting for I/O and resumes execution when the operation completes, either on a thread pool thread, or the captured SynchronizationContext.

A deadlock scenario, common in older ASP.NET or UI frameworks such as WinForms, blocks the sync-context thread. If the continuation tries to resume on the same thread, deadlock can occur:


var result = someAsync().Result;

Correct usage is always await:


var result = await someAsync(); 

In library code you may also use:


await someAsync().ConfigureAwait(false);

In ASP.NET Core there is no SynchronizationContext, so using .Result is less likely to cause deadlocks, but it still blocks a thread pool thread and reduces throughput under load.

Why this question matters:

This asynchronous programming .NET interview question evaluates whether mid-to-senior-level candidates understand how improper async/await usage leads to blocking and deadlocks that impact production stability. Hiring managers should focus on candidates who explain why using .Result or .Wait() can block threads and interfere, particularly in environments with a synchronization context. Adept candidates will demonstrate an awareness of real-world async risks that impact performance and best practices for avoiding common concurrency issues.

26.

What are the primary authentication methods for securing ASP.NET Core APIs?

View answer

Choose the authentication method based on caller type and security requirements.

JWT Bearer: A stateless approach using self-contained claims. The most common choice for APIs.


builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o => o.Authority = "https://idp.example.com");

OAuth 2.0 with OpenID Connect (OIDC): Delegate authentication to an external identity provider such as Entra ID, Auth0, or Keycloak. Best suited for SSO and user-facing applications.

API Keys: A simple shared secret in a request header, suitable for server-to-server communication or internal tooling. Should always be used over HTTPS.

mTLS: Both client and server present certificates. Commonly used in zero-trust environments and service meshes.

Rule: Never roll your own authentication, use proven libraries such as Microsoft.Identity.Web, IdentityServer, or OpenIddict.

27.

How should secrets and configuration be managed in a production .NET application?

View answer

Configuration should follow a layered hierarchy, where each layer can override the previous one.

  • appsettings.json: Non-sensitive default settings.
  • appsettings.{Environment}.json: Environment-specific, non-sensitive overrides.
  • Environment variables: Values injected by the deployment platform.
  • Azure Key Vault or AWS Secrets Manager: Store all secrets and access them via managed identities, so no credentials are stored in configuration.

Bind Azure Key Vault in Program.cs:


builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{vault}.vault.azure.net/"),
    new DefaultAzureCredential()); 

Additional rules:

  • Never commit secrets to source control.
  • Never log secrets or Personally Identifiable Information (PII).
  • Rotate secrets regularly, automate the rotation where possible.
28.

What are the trade-offs between synchronous and asynchronous programming in .NET?

View answer

Synchronous programming has a simple control flow and is easy to debug. However, the thread is blocked until the operation completes, which can lead to thread starvation in web APIs under high concurrent load.

Asynchronous programming uses async/await to release the thread during I/O, allowing the same server to handle far more concurrent requests. This is essential for scalable web APIs and background services.

Synchronous example with one thread blocked per request during I/O:


var data = db.Users.ToList();

Asynchronous example where thread is returned to the pool while the database query runs:


var data = await db.Users.ToListAsync(ct);

Common asynchronous pitfalls include:

  • .Result or .Wait() blocks a thread and risks deadlocks.
  • Forgetting to use await can result in fire-and-forget execution without proper error handling.
  • Using async void outside of event handlers means exceptions are unobservable and cannot be reliably handled.
29.

Does .NET support method overloading based on the ref keyword?

View answer

Yes. Methods with a ref parameter or a plain value parameter are considered distinct signatures by the compiler.

Example:


public void Process(int value)
{
    Console.WriteLine("By value: " + value);
}

public void Process(ref int value)
{
    Console.WriteLine("By ref: " + value);
}

In the following, (x) calls by-value overload and (ref x); calls by-ref overload:


int x = 10;

Process(x);
Process(ref x);

Note: ref and out cannot overload each other, because they are indistinguishable to the CLRs method resolution rules. Prefer out for returning values and in` (readonly reference) for large, read-only struct parameters.

30.

What is the CLR and what core services does it provide?

View answer

The CLR (Common Language Runtime) is .NET`s execution engine.

It provides:

  • JIT compilation. Converts Common Intermediate Language (CIL) to native machine code at runtime.
  • Garbage Collection. Automatic memory management.
  • Exception handling. Structured try, catch, and finally handling across languages.
  • Type safety. Enforced by the Common Type System (CTS) and the verifier.
  • Security. Code access and assembly-level permissions.

Without the CLR, managed code cannot run. It is the heart of the .NET platform.

31.

What is encapsulation in C# and how do access modifiers enforce it?

View answer

Encapsulation hides internal state and exposes only a controlled public interface, reducing coupling and accidental misuse.

Example:


public class BankAccount
{
    private decimal _balance;

    public decimal Balance => _balance;

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Must be positive");

        _balance += amount;
    }
}

Access modifiers include public, private (default for members), protected, internal, protected internal, and private protected. These control what code can access a class or its members, helping enforce encapsulation.

32.

How does inheritance work in C# and what are its limitations?

View answer

In C#, a class can inherit from one base class (single inheritance) using the : syntax, gaining access to its public and protected members.

Example:

 
public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("...");
    }
}

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof");
    }
}

Limitations:

  • C# only supports single class inheritance (no multiple inheritance).
  • Interfaces should be used when multiple-type contracts are needed.
  • Composition is preferred over inheritance when behavior varies at runtime.
33.

What is the difference between compile-time and runtime polymorphism in C#?

View answer

Compile-time (static) polymorphism: Method overloading and operator overloading, resolved by the compiler based on argument types.

Runtime (dynamic) polymorphism: Method overriding via virtual and override. The correct implementation is chosen at runtime based on the actual object type.

Example:

 
Animal a = new Dog();
 a.Speak(); 

This calls the Dog implementation of Speak(), so the output is “Woof”, even though the variable is typed as Animal. This behavior is determined at runtime.

Primary keywords: virtual (base class), override (derived class), abstract (must override in a derived class), and sealed (prevents further override).

34.

What is an interface in C# and how does it differ from an abstract class?

View answer

An interface in C# defines a contract (method and property signatures) with no implementation aside from default interface methods. A class can implement multiple interfaces.

An abstract class can contain implementation, constructors, and fields, while a class can only inherit from one abstract or base class.

 
public interface IShape
{
    double Area();
}

public class Circle : IShape
{
    public double Radius { get; init; }

    public double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

Rule: Interfaces are preferred for defining contracts and abstract classes are used when sharing implementation across a class hierarchy.

35.

What is the difference between throw and throw ex in a catch block?

View answer

Using throw (re-throw) preserves the original stack trace, so the exception appears to originate where it was first thrown.

Using throw ex resets the stack trace to the current line, losing the original call site information.

Example using throw:

 
try
{
    DoWork();
}
catch (Exception ex)
{
    Log(ex);

    throw;
}
 

Always use bare throw when re-throwing. Avoid using throw ex because it overwrites the stack trace, making debugging more difficult.

Example of adding context while preserving the original exception (wrap it):


throw new WrapperException("msg", ex);

36.

What is LINQ in .NET and what are the two syntax styles?

View answer

LINQ (Language-Integrated Query) is a set of extension methods and language features used to query collections, databases, XML, and other data sources in a uniform, declarative way.

Example data:


var nums = new[] { 1, 2, 3, 4, 5 };

Query syntax (similar to SQL):


var evens = from n in nums
            where n % 2 == 0
            select n;

Method (fluent) syntax:


var evens2 = nums.Where(n => n % 2 == 0);

Both approaches produce identical intermediate language (IL) at compile time. Method syntax is more composable for complex pipelines; query syntax is often preferred for readability in scenarios such as joins.

37.

What are the key differences between .NET Framework and modern .NET 5+?

View answer

.NET Framework: Windows-only. Ships with the operating system, is not open-source, and no longer receives feature updates.

Modern .NET 5+: Cross-platform (Windows, Linux, macOS), open-source on GitHub. Ships independently of the operating system and offers significantly better performance (especially in ASP.NET Core applications). It also supports Native AOT, .NET MAUI, and unified tooling.

Migration guidance: New projects should target modern .NET. The .NET Framework is maintained only for existing Windows applications that cannot be migrated.

38.

When should you choose an abstract class over an interface in C#?

View answer

Choose an abstract class when:

  • You need to share default implementation across derived types.
  • The hierarchy represents an “is-a” relationship.
  • You need constructors, fields, or access-controlled members.

Example:

 
public abstract class Logger
{
    protected abstract void WriteCore(string msg);

    public void Log(string msg)
    {
        Stamp(msg);
        WriteCore(msg);
    }

    private void Stamp(string m)
    {
    }
}

Choose an interface when you need a pure contract that unrelated types can implement or when multiple type contracts are needed.

39.

What are generics in C# and why are they preferable to using object?

View answer

Generics allow type-safe, reusable code without the need for boxing or casting.

Example without generics (boxing and runtime casting). This cast happens at runtime and can fail if the stored value is not the expected type.


ArrayList list = new ArrayList();
list.Add(42);

int x = (int)list[0];

Example with generics (compile-time type safety, no boxing). No cast is needed because the type is known at compile time.


List<int> list = new List<int>();
list.Add(42);

int x = list[0];

Generic constraints, such as where T : IComparable<T> can further restrict type parameters to ensure compile-time safety.

40.

How does the Single Responsibility Principle apply to a .NET service class?

View answer

SRP (Single Responsibility Principle): A class should have only one reason to change; one responsibility.

Violates SRP: The following code violates the Single Responsibility Principle. It handles business logic, persistence, and email.


public class OrderService
{
    void Place(Order o)
    {
        /* calc */
        Db.Save(o);
        Email.Send(o);
    }
}

Follows SRP: The following code follows the Single Responsibility Principle. Each class has one responsibility.


public class OrderService
{
    void Place(Order o)
    {
        /* calc */
        _repo.Save(o);
        _notifier.Notify(o);
    }
}

public class OrderRepo
{
    void Save(Order o) { /* DB only */ }
}

public class OrderNotifier
{
    void Notify(Order o) { /* email only */ }
}

In .NET, constructor-injected services make SRP easy to enforce. If a constructor has more than four dependencies, the class likely violates SRP.

41.

What is the Open-closed Principle, and how do you apply it with interfaces in C#?

View answer

OCP (Open-closed Principle): Classes should be open for extension, closed for modification. Add new behavior by adding new types, not editing existing ones.


public interface IDiscountStrategy { decimal Apply(decimal price); }
 public class SeasonalDiscount : IDiscountStrategy { public decimal Apply(decimal p) => p * 0.9m; }
 public class LoyaltyDiscount : IDiscountStrategy { public decimal Apply(decimal p) => p * 0.85m; }
  public class PriceCalculator {
   private readonly IDiscountStrategy _s;
   public PriceCalculator(IDiscountStrategy s) => _s = s;
   public decimal Calculate(decimal p) => _s.Apply(p);
  }

New discount types can be added without touching PriceCalculator.

42.

What is the Liskov Substitution Principle and how can its violation cause bugs in .NET?

View answer

LSP (Liskov Substitution Principle): A derived class must be substitutable for its base class without changing the program`s correctness.

A classic violation is ReadOnlyCollection pretending to be a list:

The following code shows an LSP violation: the base class contract implies that Add() should work, but the derived class throws an exception instead.


public class ImmutableList : List<int> {
  public new void Add(int i) => throw new NotSupportedException();
 }

Fix: Separate abstractions. Use IReadOnlyList<T> for read-only contracts instead of deriving from List<T>.

In .NET, LSP violations often surface as unexpected NotSupportedExceptions or silent behavioral changes when polymorphism is used.

43.

What is the Interface Segregation Principle? Give an example in C#.

View answer

ISP (Interface Segregation Principle): Clients should not be forced to depend on interfaces they do not use. Prefer small, focused interfaces over fat ones.

Fat interface example: The following interface is too broad because every implementer is forced to support printing, even if printing is irrelevant to that type.


public
interface IWorker {
  void Work();
  void Eat();
  void Print();
}

Segregated interface example: The following interfaces follow the Interface Segregation Principle by splitting responsibilities into smaller, focused contracts.


public interface IWorkable { void Work(); }
public interface IFeedable { void Eat(); }
public interface IPrintable { void Print(); }

Robot example: The Robot class only needs the IWorkable interface.


class Robot : IWorkable
 {
public
  void Work() { /* ... */ }
}

In .NET DI, ISP helps keep service registrations minimal and improves testability.

44.

What is the Dependency Inversion Principle and how does .NET’s DI container implement it?

View answer

DIP (Dependency Inversion Principle): High-level modules should not depend on low-level modules; both should depend on abstractions.

Violation example: The following code violates the Dependency Inversion Principle because OrderService depends directly on the concrete SqlOrderRepo implementation.


public class OrderService { private SqlOrderRepo _repo = new SqlOrderRepo(); }

Abstraction-based example: The following code follows the Dependency Inversion Principle because both components depend on the IOrderRepository abstraction.


public class OrderService 
{ 
	private readonly IOrderRepository _repo;
	public OrderService(IOrderRepository r) => _repo = r; 
}

.NET’s built-in DI container (‘IServiceCollection’) is the practical implementation of DIP:


builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

The container resolves concrete types at runtime; high-level code never references them directly.

45.

How do you configure a one-to-many relationship in EF Core using Fluent API?

View answer

EF Core can infer relationships by convention, but Fluent API gives explicit, maintainable control.

The following configuration is typically defined in DbContext.OnModelCreating.


modelBuilder.Entity<Order>()
    .HasMany(o => o.Items)
    .WithOne(i => i.Order)
    .HasForeignKey(i => i.OrderId)
    .OnDelete(DeleteBehavior.Cascade);

Convention-based configuration works for simple cases. Fluent API is preferred in production to avoid relying on implicit behavior, especially for cascade rules, table names, and column constraints.

46.

What is deferred execution in LINQ and why does it matter?

View answer

A LINQ query is not executed when it is defined. Execution is deferred until the result is enumerated (foreach, ToList(), Count(), etc.).


  IQueryable<User> query = db.Users.Where(u => u.IsActive); 

The query is defined here, but it is not executed yet. Additional filters can still be composed before execution.


  var result = query.ToList();

The query executes when ToList() is called, and SQL is sent to the database at that point.

Why it matters:

  • Allows composing queries incrementally without multiple DB round-trips.
  • Avoid enumerating twice. Each enumeration re-executes the query.
  • Use AsNoTracking() for read-only EF queries to avoid change-tracking overhead.
47.

How does attribute routing work in ASP.NET Core and when should you use conventional routing instead?

View answer

Attribute routing places route templates directly on controllers and actions, giving precise control.


 [ApiController, Route("api/[controller]")]
 public class OrdersController : ControllerBase {
  [HttpGet("{id:int}")]
  public Task<Order> Get(int id) => _svc.GetAsync(id);
 }

Conventional routing (app.MapControllerRoute) centralizes route definitions. It is preferred for MVC apps with many similar routes.

Rules:

  • Use attribute routing for APIs (explicit, discoverable, Swagger-friendly)
  • Use conventional routing for traditional MVC UIs.
48.

What are the main pattern-matching features added in C# 8-12?

View answer

Pattern matching allows concise, readable conditional logic beyond simple type checks.

Switch expression (C# 8) + property patterns (C# 8) + list patterns (C# 11)


string Describe(object obj) => obj switch {
    int n when n < 0 => "negative int",
    string { Length: 0 } => "empty string",
    null => "null",
    _ => "something else"
};

List pattern (C# 11):

This pattern matches exactly three elements.


bool IsRgb(int[] c) => c is [_, _, _]; 

Key additions:

  • is patterns
  • switch expressions
  • property/positional/list/relational patterns
49.

What are Nullable Reference Types (NRT) in C# 8+ and what problem do they solve?

View answer

NRT (Nullable Reference Types) enables the compiler to track nullability of reference types, turning potential NullReferenceException into compile-time warnings.

Enable nullable reference types globally in the .csproj file with enable.


public class User
{
    public string Name { get; set; }
    public string? Nickname { get; set; }
}
void Greet(User? user)
{
    Console.WriteLine(user?.Name ?? "Guest");
}

The compiler warns if a non-nullable property like Name is not initialized.

The nullable Nickname property is explicitly allowed to contain null.

The user?.Name ?? "Guest" expression safely handles a null value by falling back to “Guest”.

Adopt NRT in new projects and enable it progressively in legacy code to eliminate an entire class of runtime bugs.

Why this question matters:

This is a mid-level .NET developer interview question about language features that tests a candidates understanding of how to prevent common null-related bugs before runtime. Hiring managers should consider applicants who explain how Nullable Reference Types shift null safety from runtime exceptions to compile-time warnings, improving code quality. Experienced developers will highlight how this results in more predictable code and actively reduces the risk of NullReferenceException` in production systems.

50.

How does EF Core’s change tracking work and when should you disable it?

View answer

EF Core tracks every entity loaded from the DB in the DbContexts ChangeTracker. On SaveChanges()`, it generates INSERT/UPDATE/DELETE statements for modified entities.

Load and auto-track example:


var user = await db.Users.FindAsync(id);
user.Name = "Jane";
await db.SaveChangesAsync();

EF Core marks the entity as modified after the property change. When SaveChanges() is called, it generates the corresponding UPDATE statement.

When to disable tracking:


var users = await db.Users.AsNoTracking().ToListAsync();

AsNoTracking() is faster and uses less memory for read-only queries. It is especially impactful in high-throughput read endpoints.

51.

What are extension methods in C# and what are the rules for creating them?

View answer

Extension methods add new methods to existing types without modifying or deriving from them.

Rules:

  • Must be in a static class.
  • The method must be static, with the this keyword in the first parameter.

 public static class StringExtensions {
   public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
  }
 string? name = null;
 Console.WriteLine(name.IsNullOrEmpty()); 

This call returns true and demonstrates that the extension method can be invoked with instance-method syntax.

LINQ itself is built entirely from extension methods on IEnumerable<T> and IQueryable<T>.

52.

What HTTP status codes should a well-designed ASP.NET Core API return for common scenarios?

View answer

HTTP status codes communicate the outcome semantically. They are part of your API contract.

200 OK: Successful GET/PUT returning a body.

201 Created: Successful POST; include Location header pointing to the new resource.

204 No Content: Successful DELETE or PUT with no body.

400 Bad Request: Validation failure; return a ProblemDetails body.

401 Unauthorized: Missing or invalid auth credentials.

403 Forbidden: Authenticated but not authorized.

404 Not Found: Resource does not exist.

409 Conflict: State conflict (e.g., duplicate key).

500 Internal Server Error: Unexpected server fault; never expose stack traces.

53.

What are Span<T> and Memory<T> and when should you use them for performance?

View answer

Span<T> is a ref struct that represents a contiguous region of memory (stack, heap, or native) with zero allocation.

The following example slices a string without allocating a new string.


 ReadOnlySpan<char> span = "hello world".AsSpan();
 ReadOnlySpan<char> word = span.Slice(0, 5);

The slice produces “hello” without allocating a new string.

Memory<T> is the heap-compatible, async-friendly alternative (can be stored in fields and passed across await boundaries).

Use cases:

  • Parsing/serialization hot paths: Avoid string.Substring allocations.
  • Buffer manipulation: MemoryPool<T> + Memory<T> avoids repeated ArrayPool calls.
  • Not compatible with async methods inside Span<T>: Use Memory<T> there.

Why this question matters:

This is a performance-focused, senior-level .NET interview question that assesses advanced performance optimization knowledge and whether a candidate understands how to reduce allocations in critical code paths. Hiring managers should consider applicants who explain how Span<T> works with memory without additional allocations and when Memory<T> is required for scenarios that cross async boundaries. Qualified developers will demonstrate a knowledge of low-level memory optimization techniques that maintain code clarity within performance-sensitive systems.

54.

What is System.Threading.Channels and when is it preferable to a simple ConcurrentQueue?

View answer

System.Threading.Channels provides a producer/consumer pipeline with async-first APIs and built-in backpressure.

A bounded channel applies backpressure when producers outpace consumers.


var ch = Channel.CreateBounded<int>(capacity: 100);

Producer:


await ch.Writer.WriteAsync(item, ct);

Consumer:


await foreach (var item in ch.Reader.ReadAllAsync(ct)) { Process(item); }

vs ConcurrentQueue: ConcurrentQueue has no built-in awaitable reading. You need polling or SemaphoreSlim.

Channels are purpose-built for:

  • Async producer/consumer.
  • Supporting single/multiple producers and consumers.
  • Composing naturally with IAsyncEnumerable<T>.
55.

What is the N+1 query problem in EF Core and how do you fix it?

View answer

N+1 occurs when code issues one query for a list, then N individual queries for each item’s related data.

N+1 example: The following code triggers one query for orders and then additional queries for each related customer.


foreach (var order in db.Orders.ToList())
    Console.WriteLine(order.Customer.Name);

The related customer is loaded lazily for each order.

Fix: Use ‘Include()’ for eager loading when you need the related entities.


var orders = await db.Orders
    .Include(o => o.Customer)
    .ToListAsync();

This loads the related customer data with a single JOIN query.

For complex projections, use ‘Select()’ to fetch only the columns you need, and avoid loading entire entity graphs.

Why this question matters:

This mid-to-senior-level .NET developer interview question is a data access question that tests if applicants can recognize and fix common database performance issues in real applications. Hiring managers should look for candidates who can describe how the N+1 problem occurs when related data is loaded in multiple queries instead of a single optimized query. Strong candidates will explain how inefficient queries impact application performance, share resolution techniques, and discuss how they work with Entity Framework to avoid performance risks in production environments.

56.

What are Minimal APIs in ASP.NET Core and when should you prefer them over MVC controllers?

View answer

Minimal APIs (introduced in .NET 6) define HTTP endpoints directly in Program.cs with no controller class required.


 app.MapGet("/orders/{id}", async (int id, IOrderService svc) =>
  await svc.GetAsync(id) is Order o ? Results.Ok(o) : Results.NotFound());

Prefer Minimal APIs for:

  • Microservices and serverless functions where startup time and binary size matter.
  • Simple CRUD services with few cross-cutting concerns.
  • Native AOT scenarios (no reflection-heavy MVC pipeline).

Prefer MVC Controllers for:

  • Large APIs with complex action filter pipelines.
  • Teams that prefer convention-based organization.
57.

How do you benchmark .NET code correctly and what common mistakes should you avoid?

View answer

Use BenchmarkDotNet. It handles warmup, JIT noise, and statistical analysis automatically.


[MemoryDiagnoser]
public class MyBench {
[Benchmark] public string Concat() => "Hello" + " " + "World";
[Benchmark] public string Interpolate() => $"Hello World";
}
BenchmarkRunner.Run<MyBench>();

Common mistakes:

  • Benchmarking in Debug mode (JIT optimizations disabled).
  • No warmup(first run is JIT-dominated).
  • Measuring the wrong thing. Always run in Release with dotnet run -c Release.
  • Ignoring allocations. Use [MemoryDiagnoser] to track GC pressure alongside time.
58.

What is the difference between lock, Interlocked, and SemaphoreSlim in .NET concurrency?

View answer

‘lock’ (Monitor): Mutually exclusive access to a code block. Blocks the thread. Not async-safe.


lock (_gate) { _count++; }

‘Interlocked’: Atomic operations on primitive types without locking. Fastest. No thread blocking.


Interlocked.Increment(ref _count);

‘SemaphoreSlim’: A counting semaphore that supports async waiting. Use ‘SemaphoreSlim’ instead of ‘lock’ in async code.


await _sem.WaitAsync(ct);
try { await DoWorkAsync(); } finally { _sem.Release(); }

Rules:

  • Use ‘Interlocked’ for simple counters/flags.
  • Use ‘SemaphoreSlim(1,1)’ as an async mutex.
  • Use ‘lock’ only in synchronous code.
59.

What are compiled queries in EF Core and when do they provide a measurable benefit?

View answer

EF Core translates LINQ expression trees to SQL on every execution. Compiled queries cache the translation, skipping the translation cost on repeat calls.


private static readonly Func<AppDbContext, int, Task<User?>> _getUser =
   EF.CompileAsyncQuery((AppDbContext db, int id) =>
   db.Users.FirstOrDefault(u => u.Id == id));

Usage: The translation is already cached for repeated calls.


  var user = await _getUser(db, userId);

The benefit is measurable on high-frequency, simple queries executed thousands of times per second (e.g., user lookups per API request).

For complex, rarely executed queries, the overhead is negligible.

60.

What is Output Caching in ASP.NET Core 7+ and how does it differ from Response Caching?

View answer

Response Caching relies on HTTP cache headers and respects client Cache-Control directives. The server may still execute the action if the client bypasses the cache.

Output Caching is server-side. The entire response is cached on the server regardless of client headers.

In Program.cs:


 builder.Services.AddOutputCache();
 app.UseOutputCache();

On an endpoint or controller action:


app.MapGet("/products", GetProducts).CacheOutput(p =>
    p.Expire(TimeSpan.FromMinutes(5)));

Output Caching supports tag-based invalidation, vary-by rules (query string, headers), and custom policies, making it far more flexible for API scenarios.

61.

When should you use a record struct versus a regular struct in C#?

View answer

Record struct (C# 10): Value type with compiler-generated value equality, with expressions, ToString(), and deconstruction.


public record struct Point(double X, double Y);
var p1 = new Point(1, 2);
var p2 = p1 with { X = 5 };
Console.WriteLine(p1 == new Point(1, 2));

The with expression creates a non-destructive copy with one changed value.

This comparison returns true because record structs use value-based equality.

Regular struct: Value type without the compiler-generated value-equality features of a record struct. Easier to get wrong.

Use record struct when:

  • Small, immutable data (2D/3D coordinates, color, money value objects).
  • You want value equality without writing boilerplate.

Avoid:

  • Large structs (copy cost).
  • Mutable structs (confusing semantics).
62.

What is the limitation of EF Core’s standard SaveChanges for bulk operations and what are the alternatives?

View answer

EF Core generates one SQL statement per entity for INSERT/UPDATE/DELETE. For thousands of rows, this is slow.

Standard approach: This issues one SQL statement per entity, which leads to many round trips for large batches.


foreach (var p in products) { db.Products.Add(p); }
await db.SaveChangesAsync();

This results in N INSERT statements.

Alternatives:

  • EF Core 7+ ExecuteUpdateAsync() / ExecuteDeleteAsync(): Single SQL, no entity tracking.

await db.Products.Where(p => p.Stock == 0).ExecuteDeleteAsync();

  • EF Core Bulk Extensions (EFCore.BulkExtensions): Bulk INSERT/UPDATE/MERGE.

await db.BulkInsertAsync(products);

  • SqlBulkCopy: Raw ADO.NET. Fastest option for massive inserts.
63.

What is CQRS and how do you implement it in a .NET application?

View answer

CQRS (Command Query Responsibility Segregation) separates write operations (Commands) from read operations (Queries) into distinct models and handlers.

Command example: A command mutates state and usually returns nothing or a result ID.


  public record PlaceOrderCommand(Guid CustomerId, List<OrderLine> Lines);
  public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, Guid> { ... }

Query example: A query returns data and does not mutate state.


 public record GetOrderQuery(Guid OrderId);
 public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto> { ... }

MediatR is the de facto .NET library for dispatching commands and queries. CQRS enables separate read/write data stores, independent scaling, and Event Sourcing as a natural extension.

64.

What is Event Sourcing and when does it make sense to use it with .NET?

View answer

Instead of storing current state, Event Sourcing persists every state-changing event. The current state is rebuilt by replaying events.


public record OrderPlaced(Guid OrderId, DateTime At);
public record OrderShipped(Guid OrderId, DateTime At);

Rebuild state example:


var order = events.Aggregate(new Order(), (acc, e) => acc.Apply(e));

When it makes sense:

  • A full audit trail is a business requirement (finance, healthcare, compliance).
  • Time-travel debugging/temporal queries are needed.
  • Event-driven microservices using the Outbox pattern.

When to avoid:

Simple CRUD apps. The added complexity (eventual consistency, snapshot strategies, schema evolution) is rarely worth it.

65.

What is the Transactional Outbox pattern and why is it important in .NET microservices?

View answer

The Transactional Outbox pattern guarantees that a DB write and a message publish are atomic, preventing the dual-write problem (DB saved but message not sent, or vice versa).

Implementation:

  1. Write the business entity and an OutboxMessage row in the same DB transaction.
  2. A background worker (IHostedService) polls the OutboxMessages table and publishes them to the broker, then marks them as processed.

Same-transaction example:


 using var tx = await db.Database.BeginTransactionAsync();
 db.Orders.Add(order);
 db.OutboxMessages.Add(new OutboxMessage { Payload = Serialize(orderPlacedEvent) });     
 await db.SaveChangesAsync();
 await tx.CommitAsync();

Libraries:

  • MassTransit Outbox
  • NServiceBus Outbox
  • Wolverine

Why this question matters:

This is a senior-level .NET interview question that evaluates a candidate’s understanding of data consistency and reliability in distributed systems. Hiring managers should expect candidates to explain the dual-write problem, how the outbox pattern ensures reliability, and their methods for solving data consistency challenges across services. Experienced candidates will describe how they design resilient .NET microservices that maintain consistency under failure conditions.

66.

How do you model a Domain-Driven Design (DDD) Aggregate Root in C#?

View answer

An aggregate root is the single entry point for a cluster of related entities. It enforces invariants and consistency boundaries.

Aggregate root example: The Order entity acts as the aggregate root and enforces invariants for the entities inside the aggregate.


public class Order
{
    private readonly List<OrderLine> _lines = new();
    public IReadOnlyList<OrderLine> Lines => _lines;
    public OrderStatus Status { get; private set; } = OrderStatus.Draft;

    public void AddLine(Product p, int qty)
    {
        if (Status != OrderStatus.Draft) throw new InvalidOperationException("Cannot modify placed   
        order");
        _lines.Add(new OrderLine(p.Id, qty, p.Price));
    }
}

Rules:

  • All mutations go through the aggregate root (private setters, no public collections).
  • Repositories work with aggregate roots, not inner entities.
  • Aggregates should be small; large aggregates are a design smell.
67.

How do you implement distributed tracing across .NET microservices using OpenTelemetry?

View answer

OpenTelemetry is the vendor-neutral standard for traces, metrics, and logs. .NET has first-class support via System.Diagnostics.ActivitySource.

In each service’s Program.cs:


 builder.Services.AddOpenTelemetry()
   .WithTracing(b => b
   .AddAspNetCoreInstrumentation()
   .AddHttpClientInstrumentation()
   .AddEntityFrameworkCoreInstrumentation()
   .AddOtlpExporter());

The OTLP exporter can send telemetry to backends such as Jaeger, Zipkin, or Tempo.

Custom span example in business logic:


using var activity = _source.StartActivity("PlaceOrder");
activity?.SetTag("order.customerId", customerId);

Correlation IDs propagate automatically via W3C TraceContext headers across HTTP and message broker calls.

68.

What strategies reduce memory allocations in a high-throughput .NET service?

View answer

Allocations drive GC pressure. Key strategies include:

  1. Object pooling: Use Microsoft.Extensions.ObjectPool for expensive-to-create objects.
  2. ArrayPool<T>: Reuse byte/char buffers instead of allocating new arrays.
  3. Span<T> / stackalloc: Process data on the stack for small, bounded buffers.
  4. String.Create() / StringBuilder: For dynamic string construction.
  5. Struct-based enumerators and value types: Avoid heap allocations in hot loops.
  6. Record struct: Use value objects instead of classes when appropriate.

Stackalloc allocates small buffers on the stack without heap allocation.


Span<byte> buf = stackalloc byte[128];

The buffer can then be used for reading or writing without creating a heap allocation.

Profile first with dotnet-trace and dotnet-gcdump before optimizing.

69.

What is the role of an API Gateway in a .NET microservices architecture?

View answer

An API Gateway is the single entry point for all client requests. It handles:

  • Routing: Forwards requests to the correct downstream service.
  • Authentication / Authorization: Validates tokens before requests reach services.
  • Rate limiting: Protects services from overload.
  • Response aggregation: Combines multiple service calls into one response.
  • SSL termination: Terminates SSL, transforms requests, and supports circuit breaking.

In the .NET ecosystem, YARP (Yet Another Reverse Proxy) is Microsoft’s high-performance, code-configurable gateway/proxy library.

Example configuration (YARP):


builder.Services.AddReverseProxy()
     .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

Alternatives:

  • Azure API Management
  • AWS API Gateway
  • Ocelot
  • Kong
70.

How do you implement health checks in ASP.NET Core for Kubernetes readiness and liveness probes?

View answer

ASP.NET Core has built-in health check support via Microsoft.AspNetCore.Diagnostics.HealthChecks.

In Program.cs:


 builder.Services.AddHealthChecks()
  .AddDbContextCheck<AppDbContext>()
  .AddUrlGroup(new Uri("https://api.ext/"), "ext-api");

This adds an EF Core connectivity check and an external dependency check.


app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready", new() { ResponseWriter =    UIResponseWriter.WriteHealthCheckUIResponse });

The liveness endpoint stays healthy as long as the process is running.

Kubernetes uses /health/live for liveness (restart if unhealthy) and /health/ready for readiness (stop routing traffic). Never route traffic to a pod that fails its readiness probe.

71.

How do you implement rate limiting in ASP.NET Core 7+?

View answer

ASP.NET Core 7 introduced built-in rate limiting middleware via Microsoft.AspNetCore.RateLimiting.

In Program.cs, configure a fixed-window limiter:


 builder.Services.AddRateLimiter(o => o
  .AddFixedWindowLimiter("fixed", opts => {
  opts.Window = TimeSpan.FromMinutes(1);
  opts.PermitLimit = 100;
  opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
  opts.QueueLimit = 10;
  }));
 app.UseRateLimiter();

Apply the policy to an endpoint:


app.MapGet("/api/orders", GetOrders).RequireRateLimiting("fixed");

Four built-in algorithms:

  • Fixed window
  • Sliding window
  • Token bucket
  • Concurrency limiter

For distributed rate limiting (multi-node environments), you can use Redis-backed limiters via community packages.

72.

What is the IHostedService / BackgroundService pattern in .NET and when should you use it?

View answer

IHostedService defines two lifecycle methods:

  • StartAsync()
  • StopAsync()

BackgroundService is an abstract base class that simplifies long-running background work.

Example:


public class OrderProcessor : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var msg in _channel.Reader.ReadAllAsync(ct))
            await ProcessAsync(msg, ct);
    }
}
builder.Services.AddHostedService<OrderProcessor>();

Use it for polling jobs, message consumers, scheduled tasks (use Quartz.NET or .NET Worker cron triggers), and cache warmup on startup.

Always honor the CancellationToken to enable graceful shutdown.

73.

How do you enforce true immutability in a C# record and what is the difference between init and set?

View answer

init (C# 9): The property can only be set during object initialization (constructor or object initializer). After that, it is effectively read-only.

set: The property can be changed at any time after construction.

Example:


public record Invoice(Guid Id, decimal Amount);

Positional records generate init-only properties by default.

With explicit init:


public record Product
{
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; set; }
}

var p = new Product { Name = "Pen", Price = 1.5m };
p.Price = 2m;
p.Name = "X";

Name can only be assigned during initialization.

Price remains mutable because it uses set.

Assigning p.Name = "X"; after construction causes a compile-time error.

74.

What is the difference between a unit test and an integration test in .NET and when should you write each?

View answer

Unit test: Tests a single unit of logic in isolation. All dependencies are replaced with fakes or mocks. Unit tests are fast (milliseconds) and can run in CI without infrastructure.

Integration test: Tests the interaction between components (for example, your API and a real database or HTTP call). These tests are slower and require real or containerized infrastructure.

Unit test example (using Moq):


var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(r => r.GetAsync(1)).ReturnsAsync(new Order(1));
var svc = new OrderService(mockRepo.Object);
var o = await svc.GetAsync(1);
Assert.NotNull(o);

Integration test with WebApplicationFactory (ASP.NET Core):


var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders/1");
response.EnsureSuccessStatusCode();

Why this question matters:

This is a .NET interview question that tests fundamentals and evaluates whether junior-to-mid-level candidates understand the purpose and trade-offs of different testing strategies. Hiring managers should look for candidates who know how to balance speed and coverage, distinguishing between isolated tests using mocks and broader tests that validate real component interactions. Strong applicants should be able to explain how they plan a reliable testing strategy and maintain code quality over time.

75.

What is Test-Driven Development (TDD) and how does the Red-Green-Refactor cycle apply to .NET?

View answer

TDD is a discipline where you write a failing test before writing production code, then write the minimum code to pass it, and then refactor.

Red: Write a test that fails because the code doesn’t exist yet.


[Fact] 
public void Add_ReturnsSum() => Assert.Equal(5, new Calc().Add(2, 3)); 

This test fails because the implementation does not exist yet.

Green: Write the minimum production code to make it pass.


public class Calc
{
    public int Add(int a, int b) => a + b;
}

Refactor: Clean up the code without breaking the test.

Benefits in .NET: TDD forces small, testable classes, provides a regression safety net, and clarifies requirements before coding. xUnit and NUnit are common testing frameworks, and FluentAssertions can improve readability.

76.

How does the ASP.NET Core middleware pipeline work and how do you create custom middleware with short-circuiting?

View answer

The middleware pipeline is a chain of components. Each component receives an HttpContext and a next delegate. It can process the request, call next to pass control forward, and then process the response on the way back.

Order of registration in Program.cs is critical.

Custom middleware example:


public class TimingMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext ctx)
    {
        var sw = Stopwatch.StartNew();
        await next(ctx);
        ctx.Response.Headers["X-Elapsed"] = $"{sw.ElapsedMilliseconds}ms";
    }
}

This middleware calls the next component in the pipeline before adding the response header.

Short-circuiting:

Omit the await _next(ctx) call to stop the pipeline (e.g., return 401 immediately in an auth middleware).

Register with:


app.UseMiddleware<TimingMiddleware>();

77.

Explain the difference between the Stack and the Heap.

View answer

The Stack stores call frames, local variables, and method execution state. Each thread has its own stack. Value types declared as local variables are typically allocated here, but this is an implementation detail, not a rule.

The Heap stores objects managed by the Garbage Collector. Reference types are typically allocated on the heap; the stack holds only the reference (pointer) to them.

Important nuances often missed in interviews:

  • Value types are not always on the stack. They live on the heap when boxed, or when they are fields inside a reference type object.
  • The real distinction is allocation strategy and lifetime: stack memory is automatically reclaimed when a method returns; heap memory is managed by the GC.

Local value type example:


int x = 42;

Here, x is a local value type variable, so it is typically stored on the stack.

Boxing example:


object boxed = x;

Here, the value is boxed, so a copy is placed on the heap.

Reference type example:


var order = new Order();

Here, the object is allocated on the heap, while the local variable holds the reference.

78.

What is a delegate in .NET?

View answer

A delegate in .NET is similar to a function pointer in C or C++. Using a delegate allows a programmer to encapsulate a reference to a method inside a delegate object. The delegate object can then be passed to code that can call the referenced method without needing to know at compile time which method will be invoked.

In addition, a delegate can be used to create a custom event within a class.

Custom event example:


public delegate void FooDelegate();
class FooClass
{
    public event FooDelegate FooEvent;

    public void TriggerEvent()
    {
        FooEvent?.Invoke();
    }
}

Usage example:


FooClass fooObj = new FooClass();
fooObj.FooEvent += OnFooEvent;
fooObj.TriggerEvent();

void OnFooEvent()
{
    Console.WriteLine("Event triggered!");
}

This example shows how to attach a real method to the event and raise it safely.

These sample questions are intended as a starting point for your interview process. If you need additional help, explore our hiring resources—or let Toptal find the best developers, designers, marketing experts, product managers, project managers, and management consultants for you.

Submit an interview question

Submitted questions and answers are subject to review and editing, and may or may not be selected for posting, at the sole discretion of Toptal, LLC.

* All fields are required

Toptal Connects the Top 3% of Freelance Talent All Over The World.

Join the Toptal community.

Learn more