Senior .NET / C# Interview: Complete Answer Guide (Q81–Q185)
Every senior and staff-level .NET interview question answered with real-world examples — async internals, EF Core, CQRS, microservices, memory, security, and modern C# features.
This guide covers every question from Q81 to Q185 of the Senior .NET interview series — with real-world examples, code, and the reasoning behind each answer. Not just "what" but "why it matters in production."
Section 1: Async/Await Deep Dive (Q81–Q85)
Q81: How does async/await actually work under the hood?
The compiler transforms every async method into a state machine — a struct that implements IAsyncStateMachine. Each await point becomes a state. The struct holds all local variables across await boundaries.
// What you write:
async Task<string> GetUserAsync(int id)
{
var user = await _db.Users.FindAsync(id);
var orders = await _orderService.GetOrdersAsync(user.Id);
return $"{user.Name} has {orders.Count} orders";
}
// What the compiler generates (simplified):
class GetUserAsync_StateMachine : IAsyncStateMachine
{
public int _state = -1; // which await we're at
public int id; // captured parameter
private User user; // local vars survive across awaits
private List<Order> orders;
private TaskAwaiter<User> _awaiter1;
private TaskAwaiter<List<Order>> _awaiter2;
void MoveNext()
{
switch (_state)
{
case -1: // initial call
_awaiter1 = _db.Users.FindAsync(id).GetAwaiter();
if (!_awaiter1.IsCompleted) { _state = 0; return; } // suspend
goto case 0;
case 0: // resume after first await
user = _awaiter1.GetResult();
_awaiter2 = _orderService.GetOrdersAsync(user.Id).GetAwaiter();
if (!_awaiter2.IsCompleted) { _state = 1; return; }
goto case 1;
case 1: // resume after second await
orders = _awaiter2.GetResult();
// complete the task with the result
}
}
}Real-world implication: await does NOT block a thread. It registers a callback (continuation) and returns the thread to the thread pool. When the awaited work completes, the continuation is scheduled. This is why ASP.NET Core can serve thousands of concurrent requests with a small thread pool.
Q82: What is SynchronizationContext and how does it relate to await?
SynchronizationContext is a scheduler that determines which thread continuations run on after an await.
| Context | Default after await |
|---|---|
| ASP.NET Core | No context — resumes on any thread pool thread |
| WinForms / WPF | UI thread — must for UI updates |
| ASP.NET (classic) | Request thread — caused many deadlocks |
Real-world usage:
// Library code — ALWAYS use ConfigureAwait(false)
// This prevents capturing a SynchronizationContext unnecessarily
public async Task<byte[]> DownloadAsync(string url)
{
using var client = new HttpClient();
return await client.GetByteArrayAsync(url).ConfigureAwait(false);
// ConfigureAwait(false) = "don't capture context, resume on any thread pool thread"
// If you omit this in a library and the caller has a UI context,
// you risk deadlocks (see Q83)
}
// Application code — can omit ConfigureAwait(false)
// because you often DO need to resume on the original context
public async Task UpdateButtonAsync()
{
var data = await _service.GetDataAsync(); // fine, UI thread needed after
button.Text = data; // must be on UI thread
}Q83: Explain a deadlock caused by mixing sync and async code
This is one of the most common production bugs in .NET codebases that migrated from sync to async incrementally.
// The deadlock scenario (classic ASP.NET or WinForms):
// Controller (on UI/request thread, SynchronizationContext is active)
public ActionResult GetData()
{
// .Result blocks the current thread, waiting for the Task
var result = GetDataAsync().Result; // DEADLOCK
return View(result);
}
private async Task<string> GetDataAsync()
{
// await captures the SynchronizationContext
// continuation needs to run on the SAME thread that .Result is blocking
// → thread A waits for Task, Task waits for thread A → deadlock
return await _db.GetAsync();
}The fix — three options:
// Option 1: Go async all the way (correct)
public async Task<ActionResult> GetData()
{
var result = await GetDataAsync();
return View(result);
}
// Option 2: Use ConfigureAwait(false) in async method (breaks the deadlock)
private async Task<string> GetDataAsync()
{
return await _db.GetAsync().ConfigureAwait(false);
// continuation no longer needs the original context
}
// Option 3: GetAwaiter().GetResult() is still blocking — not a real fix
// but avoids AggregateException wrappingReal-world incident: A .NET Framework Web API was migrated to async database calls but the controllers weren't updated. Under load, the thread pool exhausted itself — every request thread was blocked waiting for async continuations that could never run because all threads were blocked.
Q84: What is Channel<T> and when do you use it?
Channel<T> is a thread-safe, async producer-consumer queue. Think of it as a pipe between tasks.
// Real-world: processing incoming webhook events without blocking the HTTP handler
public class WebhookProcessor
{
private readonly Channel<WebhookEvent> _channel;
private readonly IEventHandler _handler;
public WebhookProcessor(IEventHandler handler)
{
_handler = handler;
// BoundedChannel limits memory — rejects or blocks producers if full
_channel = Channel.CreateBounded<WebhookEvent>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait,
SingleWriter = false,
SingleReader = false,
});
// Start background consumers
for (int i = 0; i < 4; i++)
_ = ConsumeAsync();
}
// Called by HTTP handler — fast, non-blocking
public async ValueTask EnqueueAsync(WebhookEvent evt)
=> await _channel.Writer.WriteAsync(evt);
// Background consumer — processes one at a time
private async Task ConsumeAsync()
{
await foreach (var evt in _channel.Reader.ReadAllAsync())
{
await _handler.HandleAsync(evt);
}
}
}When to use vs alternatives:
| Scenario | Use |
|---|---|
| Producer faster than consumer, bounded backpressure | Channel<T> |
| Simple background task | BackgroundService |
| Cross-process messaging | RabbitMQ / Azure Service Bus |
| CPU-bound parallel work | Parallel.ForEachAsync |
Q85: What is the difference between parallelism and concurrency?
- Concurrency: multiple tasks making progress, potentially on one CPU (interleaving).
async/awaitis concurrency — one thread switches between tasks. - Parallelism: multiple tasks running simultaneously on multiple CPUs.
Parallel.For,Task.WhenAllwith CPU-bound work, PLINQ.
// CONCURRENCY — one thread, multiple I/O tasks in flight
async Task ConcurrentExample()
{
// These start simultaneously but a single thread manages them
var t1 = _db.GetUserAsync(1);
var t2 = _api.GetWeatherAsync("Oslo");
var t3 = _cache.GetAsync("key");
await Task.WhenAll(t1, t2, t3); // concurrent I/O
}
// PARALLELISM — multiple threads, CPU-bound work
void ParallelExample(List<Image> images)
{
// Each image processed on a separate CPU core
Parallel.ForEach(images, new ParallelOptions { MaxDegreeOfParallelism = 4 },
image => ProcessImage(image)); // CPU-intensive
}
// WRONG: Using parallelism for I/O-bound work wastes threads
void BadPattern(List<int> ids)
{
Parallel.ForEach(ids, id =>
{
// This blocks a thread per item — thread pool exhaustion
var user = _db.GetUserAsync(id).Result;
});
}Section 2: EF Core Internals (Q86–Q90)
Q86: How does EF Core change tracking work?
When you query an entity, EF Core stores a snapshot of its original values in the ChangeTracker. On SaveChanges(), it compares current values to the snapshot and generates the minimal SQL UPDATE.
// EF Core tracking states:
// Detached → Added → Unchanged → Modified → Deleted
var user = await _context.Users.FindAsync(1); // state: Unchanged
// EF stored snapshot: { Name: "Alice", Email: "alice@example.com" }
user.Name = "Alicia"; // state: Modified (detected automatically)
await _context.SaveChangesAsync();
// Generates: UPDATE Users SET Name = 'Alicia' WHERE Id = 1
// Only the changed column — not the full row
// NoTracking — use for read-only queries (faster, less memory)
var readOnlyUser = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == 1);
// No snapshot stored, cannot be saved backReal-world performance tip: In read-heavy APIs, always use AsNoTracking(). Tracking allocates memory for every entity and its snapshot. For a list of 1,000 users, that's 2,000 objects in memory instead of 1,000.
Q87: What is the difference between SaveChanges and ExecuteUpdateAsync?
SaveChanges uses change tracking — it loads entities first, modifies them in memory, then saves. ExecuteUpdateAsync (EF Core 7+) sends SQL directly without loading.
// SaveChanges approach — loads ALL matching users (N reads + 1 write)
var premiumUsers = await _context.Users
.Where(u => u.Tier == "free" && u.CreatedAt < cutoff)
.ToListAsync(); // SELECT — loads 50,000 rows into memory
foreach (var user in premiumUsers)
user.Tier = "premium";
await _context.SaveChangesAsync(); // UPDATE 50,000 rows, one at a time
// ExecuteUpdateAsync — single SQL UPDATE, no entity loading
await _context.Users
.Where(u => u.Tier == "free" && u.CreatedAt < cutoff)
.ExecuteUpdateAsync(setters => setters
.SetProperty(u => u.Tier, "premium")
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow));
// Generates: UPDATE Users SET Tier='premium', UpdatedAt=... WHERE Tier='free' AND CreatedAt < ...
// Zero entities loaded into memoryUse ExecuteUpdateAsync / ExecuteDeleteAsync when: bulk operations on many rows, no need for entity-level events, no change tracking hooks needed.
Use SaveChanges when: you need domain events, interceptors, or per-entity business logic to fire.
Q88: What is a global query filter and what are its pitfalls?
A global query filter adds a WHERE clause automatically to every query for an entity type.
// Common use: soft deletes and multi-tenancy
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Soft delete filter — automatically excludes deleted records
modelBuilder.Entity<User>()
.HasQueryFilter(u => !u.IsDeleted);
// Multi-tenant filter — scoped to current tenant
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
// Every query now automatically filters:
var users = await _context.Users.ToListAsync();
// SELECT * FROM Users WHERE IsDeleted = 0 ← filter added automatically
// Bypass when needed:
var allUsers = await _context.Users.IgnoreQueryFilters().ToListAsync();Pitfalls:
- Join confusion — filters apply to navigations too.
_context.Posts.Include(p => p.Author)filters the Author if User has a soft-delete filter. - Performance — unindexed filter columns cause full table scans on every query.
- Admin bypass forgotten — developers forget the filter exists and wonder why deleted records don't show up in admin tools.
- Tenant context not set — if
_tenantIdis null/default, you query all tenants or get empty results.
Q89: Explain the N+1 problem and all ways to solve it
Already covered in depth in our concurrency bugs article, but here's the EF Core-specific breakdown:
// N+1 — 1 query for orders + N queries for customers
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
// EF Core lazy loading fires a SELECT per order — 1000 orders = 1001 queries
Console.WriteLine(order.Customer.Name);
}
// Fix 1: Eager loading with Include
var orders = await _context.Orders
.Include(o => o.Customer) // JOIN in a single query
.ToListAsync();
// Fix 2: Split query (for large collections — avoids cartesian explosion)
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.Items) // Items could cause cartesian product
.AsSplitQuery() // EF Core 5+ — runs separate queries, joins in memory
.ToListAsync();
// Fix 3: Projection (most efficient — only select needed columns)
var result = await _context.Orders
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer.Name, // EF translates to a JOIN
ItemCount = o.Items.Count
})
.ToListAsync();
// Fix 4: Explicit loading (when you conditionally need related data)
var order = await _context.Orders.FindAsync(id);
if (needCustomer)
await _context.Entry(order).Reference(o => o.Customer).LoadAsync();Q90: What is an owned entity type in EF Core?
An owned entity is a value object in DDD — it belongs to one owner entity, has no identity of its own, and is stored in the same (or a related) table.
// Domain model
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public Address HomeAddress { get; set; } // owned entity
public Address WorkAddress { get; set; } // second instance
}
public class Address // no Id — it's a value object
{
public string Street { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
}
// Configuration
modelBuilder.Entity<User>().OwnsOne(u => u.HomeAddress, a =>
{
a.Property(x => x.Street).HasColumnName("HomeStreet");
a.Property(x => x.City).HasColumnName("HomeCity");
});
// Stored in Users table: HomeStreet, HomeCity, HomePostalCode columns
// Or in a separate table:
modelBuilder.Entity<User>().OwnsOne(u => u.HomeAddress, a =>
a.ToTable("UserAddresses"));Real-world use: Money value objects (Amount, Currency), addresses, GPS coordinates, email addresses with validation logic. Avoids creating separate tables for simple value objects that have no independent lifecycle.
Section 3: ASP.NET Core Internals (Q91–Q95)
Q91: Explain the ASP.NET Core request pipeline
Every HTTP request passes through a linear chain of middleware components. Each middleware can:
- Short-circuit (return a response without calling next)
- Pass to the next middleware
- Modify the request before and/or after the next middleware
Request
→ ExceptionHandlingMiddleware
→ HttpsRedirectionMiddleware
→ StaticFilesMiddleware
→ RoutingMiddleware
→ AuthenticationMiddleware
→ AuthorizationMiddleware
→ EndpointMiddleware (your controller action)
← [response flows back up]
←
←
←
←
←
Responsevar app = builder.Build();
// Order matters — each Use() adds to the chain
app.UseExceptionHandler("/error"); // catches exceptions from everything below
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); // matches route to endpoint
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed?
app.MapControllers(); // execute the endpoint
// Custom middleware
app.Use(async (context, next) =>
{
// Before — runs on the way IN
var sw = Stopwatch.StartNew();
await next(context);
// After — runs on the way OUT
sw.Stop();
context.Response.Headers["X-Response-Time"] = $"{sw.ElapsedMilliseconds}ms";
});Q92: How does dependency injection work in ASP.NET Core?
ASP.NET Core has a built-in IoC container. Services are registered with a lifetime, resolved through constructor injection.
// Registration — three lifetimes
builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
// One instance for the entire application lifetime
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
// One instance per HTTP request (or DI scope)
builder.Services.AddTransient<IValidator<CreateOrderDto>, CreateOrderValidator>();
// New instance every time it's resolved
// Resolution — constructor injection (preferred)
public class OrderController : ControllerBase
{
private readonly IOrderRepository _repo;
private readonly IEmailService _email;
public OrderController(IOrderRepository repo, IEmailService email)
{
_repo = repo;
_email = email;
}
}Captive dependency problem:
// BUG: Singleton capturing a Scoped service
builder.Services.AddSingleton<MyService>(); // lives forever
builder.Services.AddScoped<IDbContext, AppDbContext>(); // lives per request
public class MyService
{
// DbContext captured at first resolution — reused across ALL requests
// DbContext is not thread-safe — data corruption, connection pool issues
public MyService(IDbContext db) { ... }
}
// Fix: inject IServiceScopeFactory and create scopes manually
public class MyService
{
private readonly IServiceScopeFactory _scopeFactory;
public MyService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public async Task DoWorkAsync()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<IDbContext>();
// db lives only for this scope
}
}Q93: What are IOptions<T>, IOptionsSnapshot<T>, IOptionsMonitor<T>?
All three read configuration into a typed object, but with different update behaviour:
public class SmtpSettings
{
public string Host { get; set; }
public int Port { get; set; }
}
// Registration
builder.Services.Configure<SmtpSettings>(
builder.Configuration.GetSection("Smtp"));
// IOptions<T> — Singleton, reads config once at startup
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // same value forever
}
}
// IOptionsSnapshot<T> — Scoped, re-reads per request
// Use when config can change at runtime (feature flags, A/B settings)
public class FeatureFlagService
{
public FeatureFlagService(IOptionsSnapshot<FeatureFlags> opts)
{
var flags = opts.Value; // fresh per request
}
}
// IOptionsMonitor<T> — Singleton with change notification
// Use in background services that need live config updates
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _monitor;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> monitor)
{
_monitor = monitor;
monitor.OnChange(settings => Log.Information("Settings changed"));
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var batchSize = _monitor.CurrentValue.BatchSize; // always current
await ProcessBatchAsync(batchSize);
}
}
}Q94: What is IActionFilter and when would you use it?
Action filters run before and after a controller action executes. They're the right tool for cross-cutting concerns that are controller-specific (unlike middleware, which applies to all requests).
// Real-world: validate model state once, not in every action
public class ValidateModelFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new UnprocessableEntityObjectResult(
context.ModelState);
// short-circuits — the action method never runs
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
// Real-world: audit logging
public class AuditFilter : IAsyncActionFilter
{
private readonly IAuditService _audit;
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var before = DateTime.UtcNow;
var result = await next(); // execute the action
await _audit.LogAsync(new AuditEntry
{
Action = context.ActionDescriptor.DisplayName,
User = context.HttpContext.User.Identity?.Name,
Duration = DateTime.UtcNow - before,
StatusCode = (result.Result as ObjectResult)?.StatusCode,
});
}
}
// Register globally or per-controller
builder.Services.AddControllers(options =>
options.Filters.Add<ValidateModelFilter>());Q95: What is the difference between IMiddleware and the Use() pattern?
// Use() pattern — inline, tight coupling, no DI per-request
app.Use(async (context, next) =>
{
// context and next are all you have
await next(context);
});
// IMiddleware — DI-friendly, testable, recommended for complex middleware
public class TenantResolutionMiddleware : IMiddleware
{
private readonly ITenantResolver _resolver; // injected from DI
public TenantResolutionMiddleware(ITenantResolver resolver)
=> _resolver = resolver;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = _resolver.Resolve(context.Request.Host.Host);
context.Items["TenantId"] = tenantId;
await next(context);
}
}
// Registration
builder.Services.AddTransient<TenantResolutionMiddleware>();
app.UseMiddleware<TenantResolutionMiddleware>();Key difference: IMiddleware is resolved from DI on every request, so it can have Scoped dependencies. Convention-based middleware (Use()) is resolved once as a Singleton.
Section 4: CQRS and MediatR (Q96–Q100)
Q96: What is CQRS and why use it?
Command Query Responsibility Segregation separates operations that read data from operations that change it.
Writes → Command → CommandHandler → Database (write model)
Reads → Query → QueryHandler → Database/ReadModel (optimised projection)Real-world example — e-commerce order system:
// COMMAND — changes state, returns acknowledgement
public record PlaceOrderCommand(
string CustomerId,
List<OrderItem> Items,
Address ShippingAddress) : IRequest<Guid>;
// QUERY — reads state, no side effects
public record GetOrderHistoryQuery(
string CustomerId,
int Page,
int PageSize) : IRequest<PagedResult<OrderSummaryDto>>;
// The command handler writes to the normalised relational model
// The query handler can read from a denormalised read model, a cache,
// or even a different database altogether
// Why bother?
// - Scale reads and writes independently (most systems read 10x more than write)
// - Optimise query models for reading (no joins, pre-aggregated)
// - Simpler handlers — each does one thing
// - Clear separation of who changes state vs who observes itQ97: How do you implement CQRS with MediatR?
// 1. Define command and handler
public record CreateProductCommand(string Name, decimal Price, int Stock)
: IRequest<int>;
public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
private readonly AppDbContext _db;
public CreateProductHandler(AppDbContext db) => _db = db;
public async Task<int> Handle(
CreateProductCommand cmd,
CancellationToken ct)
{
var product = new Product
{
Name = cmd.Name,
Price = cmd.Price,
Stock = cmd.Stock,
};
_db.Products.Add(product);
await _db.SaveChangesAsync(ct);
return product.Id;
}
}
// 2. Define query and handler
public record GetProductQuery(int Id) : IRequest<ProductDto?>;
public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto?>
{
private readonly AppDbContext _db;
public Task<ProductDto?> Handle(GetProductQuery query, CancellationToken ct)
=> _db.Products
.Where(p => p.Id == query.Id)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(ct);
}
// 3. Controller dispatches through MediatR
[ApiController, Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<IActionResult> Create(CreateProductCommand cmd)
=> Ok(await _mediator.Send(cmd));
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var result = await _mediator.Send(new GetProductQuery(id));
return result is null ? NotFound() : Ok(result);
}
}Q98: What is the Pipeline Behavior in MediatR?
Pipeline behaviors wrap every request/response — like middleware but for your application layer.
// Logging behavior — logs every command/query
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var name = typeof(TRequest).Name;
_logger.LogInformation("Handling {Request}", name);
var sw = Stopwatch.StartNew();
var response = await next(); // call the actual handler
_logger.LogInformation("Handled {Request} in {Ms}ms", name, sw.ElapsedMilliseconds);
return response;
}
}
// Validation behavior — validates before handler runs
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}
// Registration order matters — outer behaviors wrap inner ones
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));Q99: What is a Domain Event and how do you implement it in .NET?
A domain event represents something that happened in the domain that other parts of the system might care about.
// Domain event — immutable record of something that happened
public record OrderPlaced(Guid OrderId, string CustomerId, decimal Total, DateTime OccurredAt);
// Aggregate raises events
public class Order
{
private readonly List<object> _domainEvents = new();
public IReadOnlyList<object> DomainEvents => _domainEvents;
public static Order Place(string customerId, List<OrderItem> items)
{
var order = new Order { /* set properties */ };
order._domainEvents.Add(new OrderPlaced(order.Id, customerId, order.Total, DateTime.UtcNow));
return order;
}
public void ClearDomainEvents() => _domainEvents.Clear();
}
// Publish events after SaveChanges — not before (DB transaction must commit first)
public class AppDbContext : DbContext
{
private readonly IMediator _mediator;
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
var result = await base.SaveChangesAsync(ct);
// Collect events from all tracked aggregates
var events = ChangeTracker.Entries<Order>()
.SelectMany(e => e.Entity.DomainEvents)
.ToList();
foreach (var entity in ChangeTracker.Entries<Order>())
entity.Entity.ClearDomainEvents();
// Publish after commit — handlers can read the saved state
foreach (var domainEvent in events)
await _mediator.Publish(domainEvent, ct);
return result;
}
}
// Handler reacts to the event
public class SendOrderConfirmationEmail : INotificationHandler<OrderPlaced>
{
public async Task Handle(OrderPlaced notification, CancellationToken ct)
=> await _emailService.SendOrderConfirmationAsync(notification.CustomerId, notification.OrderId);
}Q100: What is the Outbox Pattern?
When you save to the database AND publish an event, both must succeed or both must fail. The Outbox pattern achieves this without distributed transactions.
// Problem: if we save the order but the message broker is down, event is lost
await _db.Orders.AddAsync(order);
await _db.SaveChangesAsync(); // ← order saved
await _bus.PublishAsync(new OrderPlaced(order.Id)); // ← might fail!
// Outbox solution: save the event IN THE SAME DB TRANSACTION
public class AppDbContext : DbContext
{
public DbSet<OutboxMessage> OutboxMessages { get; set; }
}
// In the handler:
await _db.Orders.AddAsync(order);
await _db.OutboxMessages.AddAsync(new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(OrderPlaced),
Payload = JsonSerializer.Serialize(new OrderPlaced(order.Id)),
CreatedAt = DateTime.UtcNow,
ProcessedAt = null,
});
await _db.SaveChangesAsync(); // both in one transaction — atomic
// Background job polls outbox and publishes unpublished messages
public class OutboxPublisher : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var pending = await _db.OutboxMessages
.Where(m => m.ProcessedAt == null)
.OrderBy(m => m.CreatedAt)
.Take(50)
.ToListAsync(ct);
foreach (var msg in pending)
{
await _bus.PublishAsync(msg.Type, msg.Payload);
msg.ProcessedAt = DateTime.UtcNow;
}
await _db.SaveChangesAsync(ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
}
}Section 5: Performance and Memory (Q101–Q105)
Q101: What causes memory leaks in .NET?
The GC handles most memory. Leaks happen when objects are rooted (reachable) but no longer needed.
// Leak 1: Static collections that grow forever
public static class EventLog
{
public static List<string> Events = new(); // never cleared
// Every event added lives for the application lifetime
}
// Leak 2: Event handlers not removed
public class Dashboard
{
public Dashboard(DataService service)
{
service.DataUpdated += OnDataUpdated; // subscribing...
// DataService holds a reference to Dashboard via the delegate
// Even if Dashboard is "discarded", GC can't collect it
}
// Fix: implement IDisposable and remove the handler
public void Dispose() => _service.DataUpdated -= OnDataUpdated;
}
// Leak 3: HttpClient created per request
public async Task<string> GetData(string url)
{
using var client = new HttpClient(); // creates a socket per call
// Sockets don't close immediately — socket exhaustion under load
// Fix: inject IHttpClientFactory
}
// Leak 4: Timers not disposed
public class PriceUpdater
{
private Timer _timer;
public PriceUpdater() => _timer = new Timer(Update, null, 0, 1000);
// Timer is registered with the GC finalizer thread — never collected
// Fix: implement IDisposable, call _timer.Dispose()
}
// Leak 5: Cached DbContext (EF Core change tracker grows forever)
// Fixed by using scoped DbContext, not singletonQ102: What is ArrayPool<T> and MemoryPool<T>?
Renting memory from a pool instead of allocating new arrays — reduces GC pressure.
// Without pool: allocating temp buffers in a loop = GC pressure
for (int i = 0; i < 10000; i++)
{
byte[] buffer = new byte[4096]; // 40MB total allocation per loop run
await stream.ReadAsync(buffer);
ProcessBuffer(buffer);
// buffer becomes garbage, GC must collect
}
// With ArrayPool: reuse the same buffer
var pool = ArrayPool<byte>.Shared;
for (int i = 0; i < 10000; i++)
{
byte[] buffer = pool.Rent(4096); // get from pool (may be larger than 4096)
try
{
await stream.ReadAsync(buffer.AsMemory(0, 4096));
ProcessBuffer(buffer.AsSpan(0, 4096));
}
finally
{
pool.Return(buffer); // return to pool — zero GC pressure
}
}Real-world: ASP.NET Core's Kestrel uses ArrayPool internally for all request/response buffers. This is how it handles thousands of concurrent connections without overwhelming the GC.
Q103: What is the GC.Collect() controversy?
Calling GC.Collect() manually is almost always wrong.
// Why developers do it (wrong reasoning):
GC.Collect(); // "free up memory after loading large data"
GC.WaitForPendingFinalizers();
GC.Collect();
// Problems:
// 1. Interrupts the GC's optimised generational collection strategy
// 2. Promotes short-lived Gen 0 objects to Gen 1/2 prematurely
// 3. Causes a full Gen 2 collection — the most expensive kind — synchronously
// 4. Pauses the application (stop-the-world)
// When it's acceptable:
// - After a known one-time large allocation (loading a big dataset, then discarding it)
// - In tests that measure allocation
// - In tools/CLIs where pausing doesn't matter
// - After loading large caches before entering a high-throughput phase
// Better approach: understand WHY memory is growing rather than forcing collection
// Use dotnet-counters to watch GC gen0/gen1/gen2 collection ratesQ104: How do you profile memory in a .NET application?
# 1. dotnet-counters — live metrics
dotnet-counters monitor --process-id 12345 System.Runtime
# Key counters:
# gc-heap-size: total managed heap
# gen-0/1/2-gc-count: collection frequency
# loh-size: Large Object Heap (objects > 85KB — never compacted by default)
# threadpool-queue-length: async backlog
# 2. dotnet-dump — capture and analyse heap snapshot
dotnet-dump collect --process-id 12345
dotnet-dump analyze ./core_20240101
# In the analyser:
# dumpheap -stat — show objects by type and size
# dumpheap -type MyClass — find all instances of a type
# gcroot <address> — show what's keeping an object alive
# 3. dotnet-trace — CPU and allocation traces
dotnet-trace collect --process-id 12345 --providers Microsoft-DotNETRuntime:0x1:5
# 4. Visual Studio Diagnostic Tools / JetBrains dotMemory for GUI profilingReal-world debugging process:
- See memory growing in
dotnet-counters(LOH growing = large array issue) - Take a heap snapshot with
dotnet-dump - Run
dumpheap -statto find the largest object types - Run
gcroot <address>on a suspicious object to find the reference chain keeping it alive
Q105: What is Span<T> vs Memory<T> vs ArraySegment<T>?
// Span<T> — stack-only slice, zero allocation, cannot be stored on heap
// Use for synchronous, CPU-bound processing
Span<byte> ParseHeader(byte[] buffer)
{
// Slice without copying — just a pointer + length
return buffer.AsSpan(0, 8); // header is first 8 bytes
}
// Memory<T> — heap-compatible slice, can be awaited
// Use when you need to pass a slice to async code
async Task ProcessAsync(Memory<byte> data)
{
await socket.SendAsync(data); // can cross await boundaries
}
// ArraySegment<T> — legacy; same concept but older API
// Prefer Memory<T> for new code
// Real-world: parsing HTTP headers without allocation
public static bool TryParseContentLength(ReadOnlySpan<char> headerLine, out long contentLength)
{
var colon = headerLine.IndexOf(':');
if (colon < 0) { contentLength = 0; return false; }
var value = headerLine.Slice(colon + 1).Trim();
return long.TryParse(value, out contentLength);
// Zero allocation — no string created from the header
}Section 6: Security (Q106–Q108)
Q106: How do you prevent SQL injection in .NET?
// VULNERABLE — never do this
string query = $"SELECT * FROM Users WHERE Name = '{userInput}'";
// Input: ' OR '1'='1 → returns all users
// Input: '; DROP TABLE Users; -- → drops the table
// Fix 1: Parameterised queries (ADO.NET)
using var cmd = new SqlCommand("SELECT * FROM Users WHERE Name = @name", conn);
cmd.Parameters.AddWithValue("@name", userInput); // input is data, not code
// Fix 2: EF Core (always parameterised by default)
var users = await _context.Users
.Where(u => u.Name == userInput) // EF generates parameterised SQL
.ToListAsync();
// Fix 3: EF Core raw SQL — use interpolation, NOT concatenation
var users = await _context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Name = {userInput}")
.ToListAsync();
// EF generates: SELECT * FROM Users WHERE Name = @p0 with @p0 = userInput
// WRONG even with EF:
var users = await _context.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Name = '{userInput}'") // injection!
.ToListAsync();Q107: What is HTTPS enforcement and HSTS?
// HTTPS redirection — redirect HTTP to HTTPS
app.UseHttpsRedirection();
// Sends 307 Temporary Redirect from http://example.com to https://example.com
// HSTS (HTTP Strict Transport Security) — tells browsers to ONLY use HTTPS
// After the first HTTPS response, browser refuses to use HTTP for this domain
app.UseHsts();
// Sends: Strict-Transport-Security: max-age=31536000; includeSubDomains
// Configuration
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true; // submit to browsers' HSTS preload list
});
// Important: never send HSTS on HTTP responses or in development
// Once a browser receives HSTS, it will refuse HTTP even if you remove the header
// Use UseHsts() only in production environments
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}Q108: How do you store passwords securely in .NET?
// WRONG: plain text
user.Password = password;
// WRONG: MD5/SHA1/SHA256 (fast algorithms — brute-forceable)
user.Password = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
// CORRECT: ASP.NET Core Identity (use this if possible)
var hasher = new PasswordHasher<User>();
user.PasswordHash = hasher.HashPassword(user, password);
// Uses PBKDF2-HMAC-SHA256 with 100,000+ iterations by default
// CORRECT: BCrypt (alternative, widely used)
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
// workFactor: 12 = 2^12 = 4096 iterations — adjust based on server speed target (~100-300ms)
// Verification
bool valid = BCrypt.Net.BCrypt.Verify(inputPassword, user.PasswordHash);
// Key properties of secure password hashing:
// 1. Slow by design (bcrypt, PBKDF2, Argon2) — makes brute force expensive
// 2. Per-password salt — prevents rainbow table attacks
// 3. One-way — cannot reverse to original passwordSection 7: Microservices and Architecture (Q109–Q120)
Q109: What is the difference between REST and gRPC?
| Aspect | REST | gRPC |
|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/2 only |
| Format | JSON (text) | Protobuf (binary) |
| Contract | OpenAPI/Swagger (optional) | .proto file (required) |
| Streaming | Limited (SSE, WebSocket separate) | Built-in (server, client, bidirectional) |
| Browser support | Native | Needs grpc-web proxy |
| Performance | Good | ~7x faster serialisation than JSON |
| Use case | Public APIs, browser clients | Internal microservices, high-throughput |
// gRPC service definition (orders.proto)
// service OrderService {
// rpc GetOrder (GetOrderRequest) returns (OrderResponse);
// rpc StreamOrders (StreamRequest) returns (stream OrderEvent);
// }
// gRPC server in .NET
public class OrderGrpcService : OrderService.OrderServiceBase
{
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request, ServerCallContext context)
{
var order = await _db.Orders.FindAsync(request.OrderId);
return new OrderResponse { Id = order.Id, Total = (double)order.Total };
}
// Server streaming — pushes multiple responses
public override async Task StreamOrders(
StreamRequest request,
IServerStreamWriter<OrderEvent> stream,
ServerCallContext context)
{
await foreach (var evt in _eventSource.GetEventsAsync(context.CancellationToken))
await stream.WriteAsync(new OrderEvent { OrderId = evt.OrderId });
}
}Q110: What is the Circuit Breaker pattern?
Prevents a failing downstream service from causing cascading failures. After N failures, the circuit "opens" and subsequent calls fail fast without hitting the service.
// States: Closed (normal) → Open (failing fast) → Half-Open (testing recovery)
// Using Polly (v8 / Microsoft.Extensions.Resilience)
builder.Services.AddHttpClient<IInventoryService, InventoryService>()
.AddResilienceHandler("inventory-pipeline", pipeline =>
{
pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // 50% failure rate triggers open
MinimumThroughput = 10, // at least 10 requests before evaluating
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(60), // stay open for 60s
OnOpened = args =>
{
Log.Warning("Circuit breaker OPENED for inventory service");
return ValueTask.CompletedTask;
},
});
pipeline.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
});
});Real-world: Netflix's Hystrix popularised this pattern. If the recommendation service is down, the homepage still loads — it just shows generic content instead of personalised suggestions.
Q111: What is a Saga pattern?
Manages a distributed transaction across multiple services without a 2-phase commit. Each step has a compensating transaction to undo it if a later step fails.
// Order saga: Reserve inventory → Charge payment → Confirm order
// If payment fails → Release inventory (compensating transaction)
public class PlaceOrderSaga
{
public async Task<bool> ExecuteAsync(PlaceOrderCommand cmd)
{
// Step 1
var reservationId = await _inventory.ReserveAsync(cmd.Items);
if (reservationId == null) return false;
try
{
// Step 2
var chargeId = await _payment.ChargeAsync(cmd.CustomerId, cmd.Total);
if (chargeId == null)
{
// Compensate step 1
await _inventory.ReleaseReservationAsync(reservationId);
return false;
}
try
{
// Step 3
await _orders.ConfirmAsync(cmd.OrderId, reservationId, chargeId);
return true;
}
catch
{
// Compensate steps 1 and 2
await _payment.RefundAsync(chargeId);
await _inventory.ReleaseReservationAsync(reservationId);
throw;
}
}
catch
{
await _inventory.ReleaseReservationAsync(reservationId);
throw;
}
}
}Q112: What is health check and why is it important in microservices?
Health checks tell orchestrators (Kubernetes, Azure App Service) whether a service can receive traffic.
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("database")
.AddRedis(redisConnectionString, "redis")
.AddUrlGroup(new Uri("https://api.payment.com/health"), "payment-api")
.AddCheck("disk-space", () =>
{
var freeGb = DriveInfo.GetDrives().Min(d => d.AvailableFreeSpace) / 1e9;
return freeGb > 1
? HealthCheckResult.Healthy($"{freeGb:F1}GB free")
: HealthCheckResult.Degraded($"Only {freeGb:F1}GB free");
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // liveness: just returns 200 if process is running
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = _ => true // readiness: checks all dependencies
});
// Kubernetes uses these:
// livenessProbe: /health/live — restart container if failing
// readinessProbe: /health/ready — stop routing traffic if failingQ113: Design a rate limiter — what are the algorithms?
// ASP.NET Core 7+ built-in rate limiting
builder.Services.AddRateLimiter(options =>
{
// Fixed Window — N requests per time window, hard reset
options.AddFixedWindowLimiter("fixed", o =>
{
o.PermitLimit = 100;
o.Window = TimeSpan.FromMinutes(1);
o.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
o.QueueLimit = 10;
});
// Sliding Window — smoother, counts requests in rolling window
options.AddSlidingWindowLimiter("sliding", o =>
{
o.PermitLimit = 100;
o.Window = TimeSpan.FromMinutes(1);
o.SegmentsPerWindow = 6; // 10-second segments
});
// Token Bucket — allows bursts up to bucket size, refills at steady rate
// Most realistic model for API usage
options.AddTokenBucketLimiter("token-bucket", o =>
{
o.TokenLimit = 100; // max burst
o.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
o.TokensPerPeriod = 20; // refill rate
});
// Concurrency — limits simultaneous in-flight requests
options.AddConcurrencyLimiter("concurrency", o =>
{
o.PermitLimit = 50; // max 50 concurrent requests
});
});
app.UseRateLimiter();
app.MapControllers().RequireRateLimiting("sliding");Q114: What is the Actor model in .NET?
Actors are isolated units of state that communicate only through messages. No shared memory = no locks needed. Microsoft Orleans and Akka.NET implement this in .NET.
// Orleans grain (actor) — each user has their own isolated state
public interface IUserSessionGrain : IGrainWithStringKey
{
Task<int> GetActiveSessionsAsync();
Task AddSessionAsync(string sessionId);
Task RemoveSessionAsync(string sessionId);
}
[StatefulGrain(typeof(UserSessionState))]
public class UserSessionGrain : Grain<UserSessionState>, IUserSessionGrain
{
public Task<int> GetActiveSessionsAsync()
=> Task.FromResult(State.Sessions.Count);
public async Task AddSessionAsync(string sessionId)
{
State.Sessions.Add(sessionId);
await WriteStateAsync(); // persist to storage
}
}
// Client code — no locks, no concurrency issues
// Orleans guarantees single-threaded execution per grain
var grain = client.GetGrain<IUserSessionGrain>("user-123");
await grain.AddSessionAsync("session-abc");Real-world: Halo's multiplayer matchmaking runs on Orleans. Each game lobby is a grain — isolated state, no shared locks, scales to millions of concurrent users.
Q115: How do you implement idempotency in an API?
An idempotent operation produces the same result whether called once or ten times.
// Client sends Idempotency-Key header with every mutation
// POST /api/payments
// Idempotency-Key: client-generated-uuid-per-request
[HttpPost("payments")]
public async Task<IActionResult> CreatePayment(
[FromBody] CreatePaymentDto dto,
[FromHeader(Name = "Idempotency-Key")] string? idempotencyKey)
{
if (idempotencyKey != null)
{
var existing = await _cache.GetAsync<PaymentResponse>($"idem:{idempotencyKey}");
if (existing != null) return Ok(existing); // return cached result
}
var result = await _paymentService.ChargeAsync(dto);
if (idempotencyKey != null)
{
await _cache.SetAsync($"idem:{idempotencyKey}", result,
TimeSpan.FromHours(24)); // store for 24h
}
return Ok(result);
}Real-world: Stripe requires Idempotency-Key on all POST requests. If a network timeout causes the client to retry, the second call returns the original result — the customer is never double-charged.
Q116: What is event sourcing?
Instead of storing current state, store the sequence of events that produced it. State is derived by replaying events.
// Traditional: store current state
// UPDATE Orders SET Status='Shipped', UpdatedAt=... WHERE Id=1
// (history is lost)
// Event sourcing: append-only event stream
public abstract record OrderEvent(Guid OrderId, DateTime OccurredAt);
public record OrderPlaced(Guid OrderId, DateTime OccurredAt, decimal Total) : OrderEvent(OrderId, OccurredAt);
public record OrderShipped(Guid OrderId, DateTime OccurredAt, string TrackingNumber) : OrderEvent(OrderId, OccurredAt);
public record OrderCancelled(Guid OrderId, DateTime OccurredAt, string Reason) : OrderEvent(OrderId, OccurredAt);
// Derive current state by replaying
public class Order
{
public Guid Id { get; private set; }
public string Status { get; private set; }
public decimal Total { get; private set; }
public static Order Replay(IEnumerable<OrderEvent> events)
{
var order = new Order();
foreach (var evt in events)
{
order.Apply(evt);
}
return order;
}
private void Apply(OrderEvent evt)
{
switch (evt)
{
case OrderPlaced e: Id = e.OrderId; Status = "Placed"; Total = e.Total; break;
case OrderShipped e: Status = "Shipped"; break;
case OrderCancelled e: Status = "Cancelled"; break;
}
}
}Q117: What is the CAP theorem?
In a distributed system, you can guarantee at most two of: Consistency, Availability, Partition tolerance.
Since network partitions are unavoidable, the real choice is CP vs AP:
| Choice | Trade-off | Real-world example | |---|---|---| | CP (Consistent + Partition-tolerant) | Reject requests during partition | Bank account balance, inventory count | | AP (Available + Partition-tolerant) | Return stale data during partition | Product catalogue, user profile, cart |
// CP decision in .NET:
// Use distributed lock before writing critical data
// Reject the write if lock cannot be acquired (partition)
var acquired = await _lock.TryAcquireAsync("balance-update-user-123", timeout: 5s);
if (!acquired) return StatusCode(503, "Service temporarily unavailable");
// AP decision:
// Use eventual consistency — serve cached/stale data during partition
var balance = await _cache.GetOrFallbackAsync(
key: "balance-user-123",
fallback: () => _db.GetBalanceAsync(userId), // might fail during partition
staleTtl: TimeSpan.FromMinutes(5)); // serve stale if DB unreachableQ118: How do you implement distributed caching?
// IDistributedCache — abstraction over Redis, SQL Server, NCache
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = "localhost:6379");
// Usage
public async Task<UserProfile> GetProfileAsync(string userId)
{
var key = $"profile:{userId}";
var cached = await _cache.GetStringAsync(key);
if (cached != null) return JsonSerializer.Deserialize<UserProfile>(cached)!;
var profile = await _db.GetUserProfileAsync(userId);
await _cache.SetStringAsync(key,
JsonSerializer.Serialize(profile),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
SlidingExpiration = TimeSpan.FromMinutes(5), // reset TTL on access
});
return profile;
}
// Cache invalidation on write
public async Task UpdateProfileAsync(string userId, UpdateProfileDto dto)
{
await _db.UpdateAsync(userId, dto);
await _cache.RemoveAsync($"profile:{userId}"); // invalidate on change
}Q119: What is the Strangler Fig pattern?
Incrementally migrate a monolith by routing traffic to new microservices one feature at a time. The monolith shrinks as services grow — like a strangler fig tree growing around a host tree.
Traffic → API Gateway / Nginx
├── /api/users → New UserService (microservice)
├── /api/orders → New OrderService (microservice)
└── /api/* → Legacy Monolith (shrinking)// Nginx configuration for incremental migration
// location /api/users { proxy_pass http://user-service; }
// location /api/orders { proxy_pass http://order-service; }
// location / { proxy_pass http://legacy-monolith; }
// In .NET: YARP (Yet Another Reverse Proxy) as the API gateway
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
// appsettings.json routes:
// /api/users/* → http://user-service
// /api/orders/* → http://order-service
// /** → http://legacy-monolithQ120: How does Azure Service Bus compare to RabbitMQ?
| Aspect | Azure Service Bus | RabbitMQ | |---|---|---| | Hosting | Fully managed (Azure) | Self-hosted or CloudAMQP | | Protocol | AMQP 1.0, proprietary SDK | AMQP 0-9-1, MQTT, STOMP | | Message ordering | Guaranteed within sessions | Per-queue with single consumer | | Dead-letter queue | Built-in | Manual configuration | | Max message size | 256KB (Standard) / 100MB (Premium) | Configurable | | Geo-redundancy | Built-in (geo-replication) | Requires Federation plugin | | Transactions | Cross-entity atomic transactions | Per-channel | | Best for | Azure-native apps, enterprise | On-premise, multi-cloud, open source |
// Azure Service Bus sender
var client = new ServiceBusClient(connectionString);
var sender = client.CreateSender("orders");
await sender.SendMessageAsync(new ServiceBusMessage(
JsonSerializer.Serialize(new OrderPlaced(orderId)))
{
SessionId = customerId, // message ordering per customer
MessageId = orderId.ToString(), // deduplication
});
// RabbitMQ sender (MassTransit)
await _bus.Publish(new OrderPlaced(orderId),
context => context.SetRoutingKey("orders.placed"));Section 8: Expert-Level Rapid Fire (Q121–Q160)
Q121: What is IAsyncEnumerable<T>?
Streams items asynchronously — the consumer processes each item as it arrives without waiting for all items.
// Producer: yields items as they're available
public async IAsyncEnumerable<Product> StreamProductsAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var row in _db.Products.AsAsyncEnumerable().WithCancellation(ct))
yield return MapToDto(row);
}
// Consumer: processes without loading all into memory
await foreach (var product in _service.StreamProductsAsync(cancellationToken))
await ProcessProductAsync(product); // process immediately, not after all load
// API endpoint with streaming JSON
[HttpGet("stream")]
public IAsyncEnumerable<ProductDto> StreamProducts()
=> _service.StreamProductsAsync(HttpContext.RequestAborted);Q122: What is IHostedService vs BackgroundService?
BackgroundService is an abstract base class implementing IHostedService with the retry/cancellation boilerplate done.
// IHostedService: manual start/stop
public class RawHostedService : IHostedService
{
public Task StartAsync(CancellationToken ct) { /* start work */ return Task.CompletedTask; }
public Task StopAsync(CancellationToken ct) { /* cleanup */ return Task.CompletedTask; }
}
// BackgroundService: just implement ExecuteAsync
public class OrderCleanupService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _repo.DeleteOldDraftOrdersAsync();
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
builder.Services.AddHostedService<OrderCleanupService>();Q123: How do you implement a job queue in .NET?
// Simple in-process queue with Channel<T>
// Production: use Hangfire, Quartz.NET, or Azure Queue Storage
// Hangfire example
builder.Services.AddHangfire(config => config.UsePostgreSqlStorage(connStr));
builder.Services.AddHangfireServer();
// Enqueue a job
BackgroundJob.Enqueue<IEmailService>(svc =>
svc.SendWelcomeEmailAsync(userId));
// Schedule for later
BackgroundJob.Schedule<IReportService>(svc =>
svc.GenerateMonthlyReportAsync(), TimeSpan.FromHours(1));
// Recurring job
RecurringJob.AddOrUpdate<ICleanupService>("cleanup",
svc => svc.DeleteExpiredTokensAsync(),
Cron.Daily);Q124: What is the Repository and Unit of Work pattern?
// Repository: abstracts data access for one aggregate
public interface IOrderRepository
{
Task<Order?> GetAsync(Guid id, CancellationToken ct = default);
Task SaveAsync(Order order, CancellationToken ct = default);
}
// Unit of Work: groups multiple repository operations in one transaction
public interface IUnitOfWork : IDisposable
{
IOrderRepository Orders { get; }
ICustomerRepository Customers { get; }
Task CommitAsync(CancellationToken ct = default);
}
// With EF Core, DbContext IS the Unit of Work
// SaveChangesAsync() commits all tracked changes atomically
// Many teams skip the explicit UoW pattern and inject DbContext directlyQ125: What is AutoMapper and when should you avoid it?
AutoMapper maps properties between two types by convention (matching names).
// Setup
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.First} {s.Last}")));
// Usage
var dto = _mapper.Map<UserDto>(user);
// AVOID AutoMapper when:
// 1. Mapping is complex — explicit code is clearer than configuration
// 2. Performance matters — reflection-based mapping is slower than hand-written
// 3. ProjectTo<T> generates unexpected SQL — always verify the query
// 4. Small/simple mappings — manual mapping is faster to read and debug
// Alternative: Mapster (faster), records with positional constructors, manual mapping
var dto = new UserDto(user.Id, $"{user.First} {user.Last}", user.Email);Q126: What is Polly?
Polly is a resilience library — retry, circuit breaker, timeout, bulkhead, fallback, hedging.
// .NET 8+ Microsoft.Extensions.Resilience (wraps Polly v8)
builder.Services.AddHttpClient<IPaymentService, PaymentService>()
.AddStandardResilienceHandler(); // retry + circuit breaker + timeout defaults
// Custom pipeline
builder.Services.AddResiliencePipeline("my-pipeline", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(300),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
})
.AddTimeout(TimeSpan.FromSeconds(10))
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30),
});
});Q127: What is Minimal API vs Controllers performance difference?
Minimal APIs have lower per-request overhead — no ControllerBase, no model binding overhead for simple cases, no filter pipeline unless explicitly added.
// Minimal API
app.MapGet("/products/{id}", async (int id, AppDbContext db) =>
{
var p = await db.Products.FindAsync(id);
return p is null ? Results.NotFound() : Results.Ok(p);
});
// Controller
[HttpGet("{id}")]
public async Task<ActionResult<Product>> Get(int id)
{
var p = await _db.Products.FindAsync(id);
return p is null ? NotFound() : Ok(p);
}In benchmarks, Minimal APIs are ~5-10% faster for simple endpoints. The difference matters at high throughput. For complex business logic, the architectural benefit of controllers (filters, conventions) outweighs the performance difference.
Q128: What is the Result<T> pattern?
Encodes success or failure in the return type rather than using exceptions for control flow.
public class Result<T>
{
public T? Value { get; }
public string? Error { get; }
public bool IsSuccess => Error == null;
private Result(T value) => Value = value;
private Result(string error) => Error = error;
public static Result<T> Ok(T value) => new(value);
public static Result<T> Fail(string error) => new(error);
}
// Usage
public async Task<Result<Order>> PlaceOrderAsync(PlaceOrderDto dto)
{
if (dto.Items.Count == 0)
return Result<Order>.Fail("Order must have at least one item");
var stock = await _inventory.CheckAsync(dto.Items);
if (!stock.AllAvailable)
return Result<Order>.Fail("Some items are out of stock");
var order = await _repo.CreateAsync(dto);
return Result<Order>.Ok(order);
}
// Caller handles both cases without try/catch
var result = await _service.PlaceOrderAsync(dto);
if (!result.IsSuccess) return BadRequest(result.Error);
return Ok(result.Value);Q129: What is FluentValidation?
public class CreateOrderValidator : AbstractValidator<CreateOrderDto>
{
public CreateOrderValidator(IProductRepository repo)
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty()
.WithMessage("Order must have at least one item");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.ProductId).NotEmpty();
item.RuleFor(i => i.Quantity).GreaterThan(0);
});
// Async rule — DB check
RuleFor(x => x.CustomerId)
.MustAsync(async (id, ct) => await repo.ExistsAsync(id, ct))
.WithMessage("Customer not found");
}
}
// Auto-validation in ASP.NET Core (via MediatR behavior or filter)
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());Q130–Q133: Observability — OpenTelemetry, Logging, Tracing
// OpenTelemetry — unified observability
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter()) // send to Jaeger, Tempo, etc.
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter());
// Structured logging with Serilog
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.WithProperty("ServiceName", "OrderService")
.WriteTo.Console(new JsonFormatter()) // machine-readable JSON
.WriteTo.Seq("http://seq:5341")
.CreateLogger();
// Use structured properties, not string interpolation
_logger.LogInformation(
"Order {OrderId} placed by customer {CustomerId} for {Total:C}",
order.Id, order.CustomerId, order.Total);
// JSON: { "OrderId": "abc", "CustomerId": "123", "Total": 99.99 }
// Queryable! Unlike: "Order abc placed by customer 123 for $99.99"Distributed tracing: A TraceId is propagated through all service calls via HTTP headers (traceparent). Every span in every service shares the same TraceId — you can visualise the full request journey across 10 microservices in a single waterfall diagram.
Q136: How do you implement pagination with EF Core efficiently?
// Offset pagination — simple but slow on large pages
public async Task<PagedResult<Product>> GetPageAsync(int page, int size)
{
var total = await _db.Products.CountAsync();
var items = await _db.Products
.OrderBy(p => p.Id)
.Skip((page - 1) * size) // OFFSET (page-1)*size — full table scan up to offset
.Take(size)
.ToListAsync();
return new PagedResult<Product>(items, total, page, size);
}
// Cursor pagination — O(1) regardless of page depth (recommended for large datasets)
public async Task<CursorResult<Product>> GetAfterAsync(int? afterId, int size)
{
var query = _db.Products.OrderBy(p => p.Id);
if (afterId.HasValue)
query = query.Where(p => p.Id > afterId.Value); // uses index — fast
var items = await query.Take(size + 1).ToListAsync(); // +1 to know if more exist
var hasMore = items.Count > size;
return new CursorResult<Product>(
items.Take(size).ToList(),
hasMore,
hasMore ? items[^1].Id : null);
}Q138: What is Azure Managed Identity?
Managed Identity eliminates connection strings and secrets from application code. Azure assigns an identity to your service and grants it access to other Azure resources.
// Without Managed Identity (secrets in config — bad)
var conn = "Server=mydb.database.windows.net;User=admin;Password=s3cr3t";
// With Managed Identity — zero secrets
var credential = new DefaultAzureCredential(); // uses the assigned identity
// Connect to Azure SQL
var conn = new SqlConnection("Server=mydb.database.windows.net;Database=mydb;");
conn.AccessToken = await new DefaultAzureCredential()
.GetTokenAsync(new TokenRequestContext(new[] { "https://database.windows.net/.default" }));
// Connect to Azure Key Vault
var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), credential);
// Connect to Azure Service Bus
var busClient = new ServiceBusClient("mynamespace.servicebus.windows.net", credential);
// In EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"),
sqlOptions => sqlOptions.UseAzureAdAuthentication(new DefaultAzureCredential())));Q144: What is the difference between ValueTask and Task?
// Task — always allocates a heap object
public async Task<int> GetCountAsync() => await _db.CountAsync();
// Every call = 1 Task allocation on the heap, regardless of whether it's async
// ValueTask — zero allocation when result is synchronous (common in hot paths)
public ValueTask<int> GetCountAsync()
{
if (_cache.TryGet("count", out int count))
return new ValueTask<int>(count); // NO allocation — result is immediate
return new ValueTask<int>(FetchFromDbAsync()); // falls back to Task
}
// Rules:
// 1. Only use ValueTask when synchronous fast-path is common
// 2. Never await a ValueTask twice — undefined behaviour
// 3. Don't store ValueTask in a field — consume immediately
// 4. For IAsyncEnumerable<T>, always use ValueTask internallyQ147: What is GC generations?
Gen 0: newly allocated, short-lived objects (local vars, temp buffers)
→ collected most frequently, fastest (~0.1ms)
Gen 1: objects that survived Gen 0 → buffer between Gen 0 and Gen 2
Gen 2: long-lived objects (static data, large caches, HttpClient instances)
→ collected infrequently, slowest (can be seconds for large heaps)
LOH: objects > 85KB → never compacted by default → fragmentation risk// Watch GC pressure in production:
// dotnet-counters: gen-0-gc-count high = many short-lived allocations (normal)
// gen-2-gc-count high = long-lived objects getting promoted (investigate)
// loh-size growing = large array/string allocations not being freed
// Reduce Gen 2 pressure:
// - Use ArrayPool for large buffers
// - Use Span<T>/Memory<T> over new string allocations
// - Avoid closures in hot paths (they allocate on heap)Q153: What is the ProblemDetails standard?
RFC 7807 — a standard JSON format for HTTP API errors. Supported natively in ASP.NET Core 7+.
// Automatic ProblemDetails for 4xx/5xx
builder.Services.AddProblemDetails();
// Custom problem detail
app.UseExceptionHandler(exApp => exApp.Run(async context =>
{
context.Response.ContentType = "application/problem+json";
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7807",
Title = "An unexpected error occurred",
Status = 500,
Detail = "Please try again or contact support",
Instance = context.Request.Path,
Extensions = { ["traceId"] = Activity.Current?.Id }
});
}));
// Response:
// { "type": "...", "title": "...", "status": 500, "traceId": "00-abc-01" }
// Client can match on "type" to handle specific error classesSection 9: Staff/Principal Level (Q161–Q185)
Q161: How do you design a .NET system for 1 million requests per day?
1 million req/day = ~11.5 req/sec average, with peaks potentially 10x higher = ~115 req/sec.
Architecture:
- Azure App Service (or AKS) with auto-scaling (2-10 instances)
- Azure SQL with read replicas for query-heavy workloads
- Redis Cache for session, computed results (cache hit rate target: 80%+)
- Azure CDN for static assets
- Azure Service Bus for async processing (order confirmations, emails)
- Application Insights for monitoring
Scaling levers:
1. Cache aggressively — most reads don't need live DB
2. Async everything — no blocking calls in request path
3. Database: connection pooling (PgBouncer / built-in), read replicas
4. Horizontal scale: stateless services scale easily; state goes to Redis/DB
5. Rate limiting: protect against traffic spikes
6. Health checks: enable fast recovery with Kubernetes liveness/readiness probesQ162: How do you handle database migrations safely in production?
// NEVER run EF Core migrations automatically on startup in production
// app.MigrateDatabase(); // ← dangerous: runs before health checks, blocks startup
// Safe approach: separate migration step in CI/CD pipeline
// dotnet ef database update --connection "..." -- before deploying new code
// Safe migration rules:
// 1. Additive changes only (new table, new nullable column, new index)
// 2. Never drop a column in the same deploy that removes code using it
// Deploy 1: mark column unused in code
// Deploy 2: drop column (after confirming no reads/writes)
// 3. Never rename a column — add new, copy data, remove old (3 deployments)
// 4. Long-running migrations (millions of rows): use batched UPDATE, not one UPDATE
// 5. New indexes: CREATE INDEX CONCURRENTLY (Postgres) — doesn't lock table
// Blue-green deployment with migrations:
// 1. Deploy migration to DB (must be backward-compatible with old code)
// 2. Deploy new code (both old and new code work with migrated schema)
// 3. Decommission old codeQ163: What is the Specification Pattern?
Encapsulates query criteria as objects — composable, reusable, testable without a database.
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } = new();
public Expression<Func<T, object>>? OrderBy { get; protected set; }
public int? Take { get; protected set; }
public int? Skip { get; protected set; }
}
public class ActivePremiumUsersSpec : Specification<User>
{
public override Expression<Func<User, bool>> Criteria =>
u => u.IsActive && u.Tier == "premium" && u.LastLoginAt > DateTime.UtcNow.AddDays(-30);
public ActivePremiumUsersSpec()
{
OrderBy = u => u.CreatedAt;
Take = 100;
}
}
// Repository applies the specification
public async Task<List<T>> ListAsync(Specification<T> spec)
{
var query = _context.Set<T>().AsQueryable();
query = query.Where(spec.Criteria);
foreach (var include in spec.Includes)
query = query.Include(include);
if (spec.OrderBy != null) query = query.OrderBy(spec.OrderBy);
if (spec.Skip.HasValue) query = query.Skip(spec.Skip.Value);
if (spec.Take.HasValue) query = query.Take(spec.Take.Value);
return await query.ToListAsync();
}Q167: What is Vertical Slice Architecture?
Organise code by feature (user story), not by layer. Each slice contains everything needed for one feature: endpoint, handler, validator, query, DTO.
Traditional (horizontal layers):
Controllers/ → GetOrderController, CreateOrderController, ...
Services/ → OrderService, UserService, ...
Repositories/ → OrderRepository, UserRepository, ...
Vertical Slice:
Features/
Orders/
PlaceOrder/
PlaceOrderCommand.cs
PlaceOrderHandler.cs
PlaceOrderValidator.cs
PlaceOrderDto.cs
GetOrderHistory/
GetOrderHistoryQuery.cs
GetOrderHistoryHandler.cs
OrderHistoryDto.csBenefits: Each feature is self-contained — you can add, modify, or delete a feature without touching other features. Pairs naturally with CQRS and MediatR.
Q177: What are Source Generators?
Source generators run at compile time and generate additional C# code. Zero runtime cost — the generated code is compiled into the assembly.
// Use case: strongly typed IDs to prevent primitive obsession
[StronglyTypedId] // triggers source generator
public partial struct OrderId { }
// Generator produces: implicit operators, JSON converters, EF Core value converters
// Use case: serialisation (System.Text.Json source generation)
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List<OrderDto>))]
public partial class OrderJsonContext : JsonSerializerContext { }
// Generated code is AOT-compatible — no reflection at runtime
var json = JsonSerializer.Serialize(order, OrderJsonContext.Default.OrderDto);
// Use case: Regex compilation
public static partial class Validators
{
[GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
public static partial Regex EmailRegex();
}
// Generator produces compiled regex — faster than new Regex(...) at runtimeQ178–Q180: Modern C# Features (C# 12)
// Primary constructors (C# 12) — constructor parameters as class members
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
// repo and logger are available throughout the class
public async Task<Order?> GetAsync(Guid id)
{
logger.LogInformation("Getting order {Id}", id);
return await repo.GetAsync(id);
}
}
// Collection expressions (C# 12) — unified syntax for all collection types
int[] arr = [1, 2, 3];
List<string> list = ["a", "b", "c"];
Span<byte> span = [0x01, 0x02, 0x03];
int[] combined = [..arr, 4, 5, 6]; // spread operator
// Interceptors (C# 12) — intercept a method call at a specific call site
// Used by source generators to replace reflection-based code with generated code
// Not for general application code — advanced source generator scenarioQ181: What is IExceptionHandler in .NET 8?
// Replaces UseExceptionHandler(app => ...) with a clean DI-friendly interface
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public async ValueTask<bool> TryHandleAsync(
HttpContext context,
Exception exception,
CancellationToken ct)
{
_logger.LogError(exception, "Unhandled exception for {Path}", context.Request.Path);
var (status, title) = exception switch
{
ValidationException => (400, "Validation failed"),
NotFoundException => (404, "Resource not found"),
UnauthorizedException => (401, "Unauthorized"),
_ => (500, "An unexpected error occurred"),
};
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = status, Title = title,
Extensions = { ["traceId"] = Activity.Current?.Id }
}, ct);
return true; // handled — don't rethrow
}
}
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
app.UseExceptionHandler();Q182: What is keyed services in .NET 8?
Register multiple implementations of the same interface under different keys — no more factory pattern hacks.
// Register
builder.Services.AddKeyedSingleton<IEmailService, SmtpEmailService>("smtp");
builder.Services.AddKeyedSingleton<IEmailService, SendGridEmailService>("sendgrid");
// Resolve
public class NotificationService([FromKeyedServices("sendgrid")] IEmailService email)
{
// email is the SendGrid implementation
}
// Programmatic resolution
var smtp = serviceProvider.GetRequiredKeyedService<IEmailService>("smtp");Q184: What is HybridCache in .NET 9?
Combines in-process (L1) and distributed (L2) caching with stampede protection and tag-based invalidation.
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(1), // L1 TTL
};
});
public async Task<ProductDto> GetProductAsync(int id, CancellationToken ct)
{
return await _cache.GetOrCreateAsync(
key: $"product:{id}",
factory: async ct => await _db.GetProductAsync(id, ct),
tags: [$"category:{product.CategoryId}"], // for bulk invalidation
cancellationToken: ct);
}
// Invalidate all products in a category
await _cache.RemoveByTagAsync($"category:5");Stampede protection: if 100 requests simultaneously miss the same cache key, HybridCache executes the factory once and shares the result — unlike IDistributedCache which fires 100 concurrent DB calls.
Q185: How do you benchmark .NET code?
// BenchmarkDotNet — the standard .NET benchmarking library
[MemoryDiagnoser] // measures allocations
[SimpleJob(RuntimeMoniker.Net80)]
public class SerializationBenchmarks
{
private readonly Product _product = new(1, "Widget", 9.99m);
private readonly JsonSerializerOptions _opts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
[Benchmark(Baseline = true)]
public string NewtonsoftJson() => Newtonsoft.Json.JsonConvert.SerializeObject(_product);
[Benchmark]
public string SystemTextJson() => JsonSerializer.Serialize(_product, _opts);
[Benchmark]
public string SystemTextJsonSourceGen()
=> JsonSerializer.Serialize(_product, ProductJsonContext.Default.Product);
}
// Run:
// dotnet run -c Release
// Results include: Mean time, StdDev, Memory allocated, Gen0/1/2 GC collections
// Rules:
// 1. Always run in Release mode (not Debug)
// 2. Warm up before measuring (BenchmarkDotNet does this automatically)
// 3. Benchmark on the target hardware — laptop results differ from server
// 4. Measure allocations, not just time
// 5. Never use Stopwatch in a loop for benchmarking — use BenchmarkDotNetQuick Reference: The 10 Patterns Every Senior .NET Engineer Must Know
| Pattern | Library | Use When | |---|---|---| | Retry + Circuit Breaker | Polly / Microsoft.Extensions.Resilience | Calling external services | | CQRS + Pipeline Behaviors | MediatR | Complex domain with many commands | | Outbox | Custom / NServiceBus / MassTransit | Reliable event publishing with DB writes | | Idempotency Key | Custom middleware | Payment APIs, retry-safe mutations | | Result pattern | Custom / FluentResults | Business errors without exceptions | | Specification | Custom | Complex reusable query criteria | | Repository + UoW | Custom with EF Core | Abstracting persistence (debatable with EF) | | Saga | MassTransit / NServiceBus | Multi-step distributed transactions | | Vertical Slice | MediatR + folder structure | Feature-centric team organisation | | HybridCache | .NET 9 HybridCache | High-throughput read-heavy workloads |
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.