📡 Expert ASP.NET Web API Interview Questions
This lesson targets senior engineers and architects. Topics include the ASP.NET Core pipeline internals, source generators, Keyed DI, Problem Details, gRPC with ASP.NET Core, OpenTelemetry, multi-tenancy, domain events, the Clean Architecture, performance profiling, .NET 8/9 features, and production deployment patterns. These questions reveal whether you understand the .NET platform deeply or just use it.
Questions & Answers
01 How does the ASP.NET Core request pipeline work internally? ►
Internals The ASP.NET Core pipeline is a chain of middleware delegates. Each middleware receives an HttpContext, optionally calls the next delegate (forwarding the request), and optionally processes the response on the way back.
// Internally, each middleware is a Func<RequestDelegate, RequestDelegate>
// RequestDelegate = Func<HttpContext, Task>
// The pipeline is built as a linked list of delegates at startup:
// Request โ M1 โ M2 โ M3 โ Endpoint โ M3 โ M2 โ M1 โ Response
// When you write:
app.Use(async (ctx, next) =>
{
// Before request handling (runs top to bottom)
Console.WriteLine("Before");
await next(ctx); // โ calls the next middleware in the chain
// After request handling (runs bottom to top on the way back)
Console.WriteLine("After");
});
// UseWhen โ conditionally branch the pipeline
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"),
branch => branch.UseMiddleware<ApiLoggingMiddleware>());
// Map โ branch and never rejoin
app.Map("/health", healthApp => healthApp.Run(ctx =>
ctx.Response.WriteAsync("OK")));
// Run โ terminal middleware (short-circuit, no next call)
app.Run(ctx => ctx.Response.WriteAsync("Default response"));
// Endpoint middleware (MapControllers) extracts the route + action
// at the end of the pipeline chain via IEndpointRouteBuilder
// The routing middleware (UseRouting) stores the matched endpoint in
// HttpContext.Features.Get<IEndpointFeature>()
// UseAuthorization and other middleware check this feature
02 What are source generators in .NET and how do they benefit ASP.NET Core? ►
Internals Source generators are a C# compiler feature that runs code during compilation to generate additional C# source files. They move work from runtime reflection to compile time, improving startup performance and enabling AOT (Ahead-of-Time) compilation.
ASP.NET Core source generators:
- Minimal API source generator โ generates request delegate code for minimal API endpoints at compile time, replacing runtime reflection with generated code
- JSON source generation โ
System.Text.Jsongenerates type-specific serialisers at compile time for faster, AOT-compatible JSON - Regex source generator โ
[GeneratedRegex]generates optimal regex implementations - LoggerMessage source generator โ generates high-performance logging methods
// JSON source generation โ opt-in serialiser context
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(ProblemDetails))]
internal partial class AppJsonContext : JsonSerializerContext { }
// Register in Program.cs
builder.Services.ConfigureHttpJsonOptions(opts =>
opts.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
// LoggerMessage source generator (zero-allocation, no boxing)
public static partial class Log
{
[LoggerMessage(EventId = 1001, Level = LogLevel.Information,
Message = "Order {OrderId} created for customer {CustomerId}")]
public static partial void OrderCreated(ILogger logger, int orderId, int customerId);
}
// Usage: Log.OrderCreated(logger, order.Id, order.CustomerId);
// Enables Native AOT publishing
// dotnet publish -r linux-x64 --self-contained -p:PublishAot=true
03 What is Keyed Dependency Injection in .NET 8+? ►
.NET 8+ Keyed DI allows multiple implementations of the same interface to be registered under different keys, and injected by key. Previously you needed factory patterns or custom resolvers to achieve this.
// Register multiple implementations with keys
builder.Services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
builder.Services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
builder.Services.AddKeyedScoped<IPaymentProcessor, BraintreeProcessor>("braintree");
// Inject by key in constructor
public class CheckoutService(
[FromKeyedServices("stripe")] IPaymentProcessor stripe,
[FromKeyedServices("paypal")] IPaymentProcessor paypal
)
{
public async Task ProcessAsync(Order order)
{
var processor = order.PaymentMethod switch
{
"stripe" => stripe,
"paypal" => paypal,
_ => throw new ArgumentException("Unknown payment method")
};
await processor.ChargeAsync(order);
}
}
// Or resolve by key from the container at runtime
public class DynamicCheckout(IServiceProvider sp)
{
public async Task ProcessAsync(Order order)
{
var processor = sp.GetRequiredKeyedService<IPaymentProcessor>(order.PaymentMethod);
await processor.ChargeAsync(order);
}
}
// Keyed singletons (e.g., named connection pools)
builder.Services.AddKeyedSingleton<IDbConnectionFactory, MasterDbFactory>("master");
builder.Services.AddKeyedSingleton<IDbConnectionFactory, ReadReplicaFactory>("read-replica");
04 What is Problem Details (RFC 7807) and how do you implement it? ►
API Design RFC 7807 defines a standard JSON format for HTTP API error responses. ASP.NET Core 7+ has built-in Problem Details support through IProblemDetailsService.
// Standard Problem Details shape:
// { "type": "...", "title": "...", "status": 404,
// "detail": "...", "instance": "/api/products/99", "traceId": "..." }
// Program.cs โ enable Problem Details
builder.Services.AddProblemDetails();
// Customise Problem Details
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions["traceId"] =
Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier;
ctx.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
if (ctx.Exception is ValidationException ve)
{
ctx.ProblemDetails.Title = "Validation failed";
ctx.ProblemDetails.Status = 422;
ctx.ProblemDetails.Extensions["errors"] =
ve.Errors.Select(e => new { e.PropertyName, e.ErrorMessage });
}
};
});
app.UseExceptionHandler();
app.UseStatusCodePages(); // adds Problem Details for 4xx with no body
// Return Problem Details from action
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var p = _repo.Find(id);
if (p == null)
return Problem(
title: "Product not found",
detail: $"No product with ID {id} exists",
statusCode: 404,
type: "https://tools.ietf.org/html/rfc7231#section-6.5.4"
);
return p;
}
// Validation Problem Details (built-in when [ApiController] is used)
// Automatically returns { "type": ".../validation-errors", "errors": {...} }
05 What is gRPC and how do you build a gRPC service with ASP.NET Core? ►
gRPC gRPC (Google Remote Procedure Call) is a high-performance RPC framework using Protocol Buffers (binary serialisation) and HTTP/2. Ideal for internal microservice communication where performance matters.
// Install: dotnet add package Grpc.AspNetCore
// 1. Define the contract in a .proto file (products.proto)
// syntax = "proto3";
// service ProductService {
// rpc GetById (GetByIdRequest) returns (ProductResponse);
// rpc GetAll (Empty) returns (stream ProductResponse); // server streaming
// rpc Create (CreateRequest) returns (ProductResponse);
// }
// message GetByIdRequest { int32 id = 1; }
// message ProductResponse { int32 id = 1; string name = 2; double price = 3; }
// 2. Implement the service
public class ProductGrpcService(AppDbContext db) : ProductService.ProductServiceBase
{
public override async Task<ProductResponse> GetById(
GetByIdRequest request, ServerCallContext context)
{
var product = await db.Products.FindAsync(request.Id);
if (product == null)
throw new RpcException(new Status(StatusCode.NotFound, "Product not found"));
return new ProductResponse { Id = product.Id, Name = product.Name, Price = (double)product.Price };
}
// Server-streaming: stream multiple responses
public override async Task GetAll(
Google.Protobuf.WellKnownTypes.Empty request,
IServerStreamWriter<ProductResponse> responseStream,
ServerCallContext context)
{
await foreach (var p in db.Products.AsAsyncEnumerable())
await responseStream.WriteAsync(new ProductResponse { Id = p.Id, Name = p.Name });
}
}
// 3. Register in Program.cs
builder.Services.AddGrpc();
app.MapGrpcService<ProductGrpcService>(); // maps to /ProductService/GetById etc.
// gRPC vs REST: gRPC is 5-7x faster, binary, strongly typed, streaming support
// Use gRPC for internal microservice communication; REST for public APIs
06 How do you implement OpenTelemetry in ASP.NET Core? ►
Observability
// Install: dotnet add package OpenTelemetry.Extensions.Hosting
// OpenTelemetry.Instrumentation.AspNetCore
// OpenTelemetry.Instrumentation.Http
// OpenTelemetry.Instrumentation.SqlClient
// OpenTelemetry.Exporter.OpenTelemetryProtocol
// Program.cs
builder.Services.AddOpenTelemetry()
.ConfigureResource(res => res.AddService(
serviceName: "order-service",
serviceVersion: "2.1.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation(opts =>
{
opts.RecordException = true;
opts.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(opts => opts.SetDbStatementForText = true)
.AddOtlpExporter(opts =>
opts.Endpoint = new Uri("http://otel-collector:4317")))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter())
.WithLogging(logging => logging
.AddOtlpExporter());
// Custom spans in business logic
private static readonly ActivitySource ActivitySource = new("OrderService");
public async Task<Order> CreateOrderAsync(CreateOrderDto dto)
{
using var activity = ActivitySource.StartActivity("CreateOrder");
activity?.SetTag("order.customer_id", dto.CustomerId);
activity?.SetTag("order.item_count", dto.Items.Count);
try
{
var order = await _repo.CreateAsync(dto);
activity?.SetTag("order.id", order.Id);
activity?.SetStatus(ActivityStatusCode.Ok);
return order;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
07 What is Clean Architecture and how do you structure an ASP.NET Core project with it? ►
Architecture Clean Architecture (Robert C. Martin) organises code in concentric layers where inner layers have no dependencies on outer layers. The domain model is at the centre, surrounded by application logic, then infrastructure, with the UI/API at the outer edge.
// Solution structure
MyApp.sln
โโโ src/
โ โโโ MyApp.Domain/ // Core domain โ no external dependencies
โ โ โโโ Entities/ // Order, Product, Customer
โ โ โโโ ValueObjects/ // Money, Address
โ โ โโโ Aggregates/
โ โ โโโ DomainEvents/ // OrderPlaced, PaymentReceived
โ โ โโโ Interfaces/ // IOrderRepository (defined here, implemented in Infra)
โ โ โโโ Exceptions/ // DomainException, InsufficientStockException
โ โ
โ โโโ MyApp.Application/ // Use cases โ depends on Domain only
โ โ โโโ Commands/ // CreateOrderCommand + Handler
โ โ โโโ Queries/ // GetOrderByIdQuery + Handler
โ โ โโโ DTOs/ // OrderDto, CreateOrderDto
โ โ โโโ Interfaces/ // IEmailService, ICacheService
โ โ โโโ Behaviours/ // MediatR pipeline behaviours (logging, validation)
โ โ
โ โโโ MyApp.Infrastructure/ // External concerns โ EF Core, Redis, email
โ โ โโโ Persistence/ // AppDbContext, Migrations, EF Repositories
โ โ โโโ Caching/ // RedisCacheService
โ โ โโโ Email/ // SendGridEmailService
โ โ โโโ DependencyInjection.cs // registers infra services
โ โ
โ โโโ MyApp.API/ // ASP.NET Core Web API โ depends on Application
โ โโโ Controllers/ // Thin โ just dispatches via MediatR
โ โโโ Middleware/
โ โโโ Filters/
โ โโโ Program.cs
โ
โโโ tests/
โโโ MyApp.Domain.Tests/
โโโ MyApp.Application.Tests/
โโโ MyApp.API.IntegrationTests/
Dependency rule: Domain โ Application โ Infrastructure โ API. The arrow shows allowed dependency direction. Infrastructure implements Application interfaces; neither knows about the API layer.
08 How do you implement domain events in ASP.NET Core with MediatR? ►
Architecture Domain events capture something that happened in the domain and allow side effects (notifications, projections, audit logs) to be handled by separate handlers โ decoupling the domain from its side effects.
// Domain event interface
public interface IDomainEvent : INotification { }
// Domain event
public record OrderPlacedEvent(int OrderId, int CustomerId, decimal Total) : IDomainEvent;
// Domain entity raises events
public class Order
{
private readonly List<IDomainEvent> _events = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _events.AsReadOnly();
public void Place(Customer customer)
{
Status = OrderStatus.Pending;
_events.Add(new OrderPlacedEvent(Id, customer.Id, Total));
}
public void ClearDomainEvents() => _events.Clear();
}
// Event handlers (side effects)
public class SendOrderConfirmationEmail : INotificationHandler<OrderPlacedEvent>
{
public async Task Handle(OrderPlacedEvent e, CancellationToken ct)
=> await _email.SendOrderConfirmationAsync(e.CustomerId, e.OrderId);
}
public class UpdateInventoryOnOrderPlaced : INotificationHandler<OrderPlacedEvent>
{
public async Task Handle(OrderPlacedEvent e, CancellationToken ct)
=> await _inventory.ReserveItemsAsync(e.OrderId);
}
// Dispatch events after SaveChanges in a MediatR pipeline behaviour
public class DomainEventDispatchBehaviour<TRequest, TResponse>(
AppDbContext db, IMediator mediator)
: IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var response = await next(); // execute handler + SaveChanges
// Collect and dispatch all raised domain events
var entities = db.ChangeTracker.Entries<Order>()
.Select(e => e.Entity).Where(e => e.DomainEvents.Any()).ToList();
foreach (var entity in entities)
{
var events = entity.DomainEvents.ToList();
entity.ClearDomainEvents();
foreach (var evt in events)
await mediator.Publish(evt, ct);
}
return response;
}
}
09 How do you implement multi-tenancy in ASP.NET Core Web API? ►
Architecture
// Tenant resolution middleware
public class TenantMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, ITenantResolver resolver)
{
// Resolve tenant from subdomain, header, or JWT claim
var tenantId = await resolver.ResolveAsync(context);
if (tenantId == null)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = "Tenant not identified" });
return;
}
context.Items["TenantId"] = tenantId;
await next(context);
}
}
// Tenant resolver implementations
public class HeaderTenantResolver : ITenantResolver
{
public Task<string?> ResolveAsync(HttpContext ctx)
=> Task.FromResult(ctx.Request.Headers["X-Tenant-Id"].FirstOrDefault());
}
// Per-tenant DbContext via query filter
public class AppDbContext(DbContextOptions opts, IHttpContextAccessor http)
: DbContext(opts)
{
private string? TenantId => http.HttpContext?.Items["TenantId"]?.ToString();
protected override void OnModelCreating(ModelBuilder mb)
{
// Global query filter โ all queries automatically filter by tenant
mb.Entity<Order>().HasQueryFilter(o => o.TenantId == TenantId);
mb.Entity<Product>().HasQueryFilter(p => p.TenantId == TenantId);
}
}
// Separate database per tenant (highest isolation)
public class TenantDbContextFactory(IHttpContextAccessor http, IConfiguration config)
{
public AppDbContext CreateContext()
{
var tenantId = http.HttpContext?.Items["TenantId"]?.ToString()
?? throw new InvalidOperationException("No tenant");
var connStr = config[$"Tenants:{tenantId}:ConnectionString"]
?? throw new InvalidOperationException($"No connection string for tenant {tenantId}");
var opts = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connStr).Options;
return new AppDbContext(opts, http);
}
}
10 What are the .NET 8 and .NET 9 major features for ASP.NET Core? ►
.NET 8/9
.NET 8 (November 2023) โ LTS:
- Native AOT for Web API โ publish self-contained, natively-compiled executables with no JIT startup overhead; ideal for serverless and containers
- Keyed DI โ register and inject multiple implementations of the same interface by key
- IExceptionHandler โ clean, composable exception handling interface
- Frozen collections โ immutable, optimised read-only collections
- Primary constructors on classes โ shorter, cleaner DI injection syntax
- Collection expressions โ
int[] x = [1, 2, 3] - System.Text.Json improvements โ
JsonSerializerOptions.MakeReadOnly(),JsonObject - Request timeout middleware โ built-in
app.UseRequestTimeouts() - Short-circuit middleware โ
app.MapGet(...).ShortCircuit()
.NET 9 (November 2024) โ STS:
- HybridCache โ L1 (in-memory) + L2 (Redis) caching with a single API; prevents cache stampedes
- OpenAPI built-in โ
builder.Services.AddOpenApi()without Swashbuckle - LINQ improvements โ
CountBy,AggregateBy,Index - Task.WhenEach โ process tasks as they complete
- Params spans โ zero-allocation variadic methods
- Numeric ordering for strings โ
CompareOptions.NumericOrdering
11 What is HybridCache in .NET 9 and how does it improve on IDistributedCache? ►
.NET 9 HybridCache (Microsoft.Extensions.Caching.Hybrid, .NET 9) is a two-level cache โ Level 1 is in-process (fast, zero network), Level 2 is distributed (Redis/SQL). It also prevents cache stampedes by deduplifying concurrent requests for the same missing key.
// Install: dotnet add package Microsoft.Extensions.Caching.Hybrid
// Program.cs
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1_024 * 1_024; // 1MB per entry
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5), // L2 expiry
LocalCacheExpiration = TimeSpan.FromMinutes(1) // L1 (in-memory) expiry
};
});
builder.Services.AddStackExchangeRedisCache(opts =>
opts.Configuration = builder.Configuration["Redis"]);
// Usage โ simpler than IMemoryCache + IDistributedCache combo
public class ProductService(HybridCache cache, AppDbContext db)
{
public async Task<Product?> GetByIdAsync(int id, CancellationToken ct = default)
{
return await cache.GetOrCreateAsync(
key: $"product:{id}",
factory: async ct => await db.Products.FindAsync([id], ct),
cancellationToken: ct
);
// Concurrent requests for the same missing key are deduplicated โ
// only ONE factory call is made; all waiters share the result
}
public async Task InvalidateAsync(int id)
=> await cache.RemoveAsync($"product:{id}");
// Tag-based invalidation (invalidate all products)
public async Task<Product?> GetByIdWithTagsAsync(int id, CancellationToken ct = default)
=> await cache.GetOrCreateAsync($"product:{id}",
factory: async ct => await db.Products.FindAsync([id], ct),
tags: ["products"], // group by tag
cancellationToken: ct);
public async Task InvalidateAllProductsAsync()
=> await cache.RemoveByTagAsync("products");
}
12 How do you profile an ASP.NET Core application in production? ►
Performance
// 1. dotnet-counters โ live performance counters (zero overhead)
dotnet-counters monitor --process-id <pid> System.Runtime Microsoft.AspNetCore.Hosting
// Key counters:
// requests-per-second, current-requests, failed-requests
// gc-heap-size, gen-0/1/2-gc-count, threadpool-queue-length
// 2. dotnet-trace โ record a trace for offline analysis
dotnet-trace collect --process-id <pid> --output trace.nettrace
dotnet-trace convert trace.nettrace --format speedscope
// Open speedscope.app for flame graph visualisation
// 3. PerfView โ deep CPU and memory analysis (Windows)
// 4. MiniProfiler โ in-request profiling with timeline (dev/staging)
// Install: dotnet add package MiniProfiler.AspNetCore.Mvc
builder.Services.AddMiniProfiler(options =>
options.RouteBasePath = "/profiler"
).AddEntityFramework();
app.UseMiniProfiler();
// Visit /profiler/results-index to see recent request timelines
// 5. Application Performance Monitoring (APM) โ production
// Azure Application Insights, Datadog APM, New Relic, Elastic APM
builder.Services.AddApplicationInsightsTelemetry();
// 6. BenchmarkDotNet โ micro-benchmarks for specific methods
[MemoryDiagnoser, SimpleJob(RuntimeMoniker.Net80)]
public class SerializationBenchmarks
{
[Benchmark] public string SystemTextJson() => JsonSerializer.Serialize(product);
[Benchmark] public string Newtonsoft() => Newtonsoft.Json.JsonConvert.SerializeObject(product);
}
// dotnet run -c Release -- --filter "*"
13 How do you implement idempotency for POST requests in ASP.NET Core? ►
API Design
// Idempotency filter โ cache responses by Idempotency-Key header
public class IdempotencyFilter(IDistributedCache cache) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
{
var key = ctx.HttpContext.Request.Headers["Idempotency-Key"].FirstOrDefault();
// No key or non-mutating method โ pass through
if (key == null || ctx.HttpContext.Request.Method is "GET" or "HEAD")
{
await next();
return;
}
var cacheKey = $"idempotency:{key}:{ctx.HttpContext.Request.Path}";
// Check cache
var cached = await cache.GetStringAsync(cacheKey);
if (cached != null)
{
var data = JsonSerializer.Deserialize<CachedResponse>(cached)!;
ctx.Result = new ContentResult
{
Content = data.Body,
ContentType = "application/json",
StatusCode = data.StatusCode
};
ctx.HttpContext.Response.Headers["X-Idempotency-Cached"] = "true";
return;
}
// Execute and capture the response
var result = await next();
if (result.Result is ObjectResult { StatusCode: < 300 } objResult)
{
var body = JsonSerializer.Serialize(objResult.Value);
var payload = JsonSerializer.Serialize(new CachedResponse(objResult.StatusCode ?? 200, body));
await cache.SetStringAsync(cacheKey, payload,
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) });
}
}
}
// Register globally or per-controller
builder.Services.AddControllers(opts =>
opts.Filters.Add<IdempotencyFilter>());
record CachedResponse(int StatusCode, string Body);
14 What are common ASP.NET Core Web API anti-patterns and how do you fix them? ►
Best Practices
- Fat controllers โ business logic in controller actions. Fix: move to service/handler layer; controllers should only bind/validate input, call a service, and return a response.
- Exposing domain models directly โ returning EF entities from API actions. Fix: always map to DTOs to control the response shape and prevent over-posting.
- Synchronous I/O in async controller โ calling
.Resultor.Wait()on Tasks. Fix: always useawait; these cause deadlocks in some ASP.NET synchronisation contexts. - Not using AsNoTracking() for read-only queries. Fix: add
.AsNoTracking()to EF queries that don’t callSaveChanges(). - Hardcoding configuration โ connection strings in source code. Fix: use IOptions<T> with appsettings / environment variables.
- Missing cancellation token propagation โ async methods don’t accept/pass
CancellationToken. Fix: always acceptCancellationToken ct = defaultand pass it to every awaitable call. - Returning 200 for all responses โ using OK() even for created resources. Fix: use 201 Created with location header for POST, 204 No Content for DELETE/PUT with no body.
- Registering DbContext as Singleton โ EF’s DbContext is not thread-safe. Fix: always register as Scoped.
// โ Fat controller
[HttpPost]
public IActionResult Create(OrderDto dto)
{
var order = new Order(dto.CustomerId);
foreach (var item in dto.Items)
order.AddItem(item.ProductId, item.Quantity);
// ... 50 more lines of business logic in the controller ...
}
// โ
Thin controller
[HttpPost]
public async Task<ActionResult<OrderDto>> Create(CreateOrderCommand cmd)
=> await _mediator.Send(cmd) is var result
? CreatedAtAction(nameof(GetById), new { id = result.Id }, result)
: BadRequest();
15 How do you containerise and deploy an ASP.NET Core Web API with Docker and Kubernetes? ►
Deployment
# Dockerfile โ multi-stage build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["src/MyApp.API/MyApp.API.csproj", "src/MyApp.API/"] COPY ["src/MyApp.Application/MyApp.Application.csproj", "src/MyApp.Application/"] RUN dotnet restore "src/MyApp.API/MyApp.API.csproj" COPY . . WORKDIR "/src/src/MyApp.API" RUN dotnet publish -c Release -o /app/publish FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app EXPOSE 8080 # Run as non-root for security RUN adduser --disabled-password appuser && chown -R appuser /app USER appuser COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyApp.API.dll"]
# Kubernetes deployment
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myregistry/myapp:v2.1.0
ports: [{containerPort: 8080}]
env:
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef: {name: db-secret, key: connection-string}
- name: ASPNETCORE_ENVIRONMENT
value: Production
resources:
requests: {cpu: "100m", memory: "128Mi"}
limits: {cpu: "500m", memory: "512Mi"}
readinessProbe:
httpGet: {path: /health/ready, port: 8080}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: {path: /health/live, port: 8080}
initialDelaySeconds: 10
periodSeconds: 30
16 What is Aspire and how does it simplify .NET cloud-native development? ►
.NET Aspire .NET Aspire (GA in .NET 8) is an opinionated cloud-native stack for building distributed .NET applications. It provides service orchestration, built-in components for common cloud resources, and a developer dashboard for observability.
// AppHost project โ orchestrates all services
var builder = DistributedApplication.CreateBuilder(args);
// Add infrastructure resources
var postgres = builder.AddPostgres("postgres")
.AddDatabase("ordersdb");
var redis = builder.AddRedis("redis");
var rabbitmq = builder.AddRabbitMQ("messaging");
// Add application services (references infrastructure)
var api = builder.AddProject<Projects.OrdersApi>("orders-api")
.WithReference(postgres) // injects connection string automatically
.WithReference(redis)
.WithReference(rabbitmq)
.WithExternalHttpEndpoints();
var worker = builder.AddProject<Projects.OrderWorker>("order-worker")
.WithReference(postgres)
.WithReference(rabbitmq);
builder.Build().Run();
// In services โ use Aspire client integrations
// dotnet add package Aspire.StackExchange.Redis.OutputCaching
builder.AddRedisOutputCache("redis");
// dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
builder.AddNpgsqlDbContext<AppDbContext>("ordersdb");
// Dashboard (localhost:18888 in dev) provides:
// - Service map with health status
// - Structured logs from all services
// - Traces across service boundaries
// - Metrics and resource usage
// .NET Aspire components: Redis, PostgreSQL, MySQL, MongoDB, Azure Service Bus,
// Azure Storage, RabbitMQ, Kafka, Elasticsearch, and more
17 What is request deduplication and how do you implement it with EF Core optimistic concurrency? ►
Concurrency
// Optimistic concurrency with EF Core โ row version / timestamp
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
[Timestamp] // automatically maps to SQL rowversion
public byte[] RowVersion { get; set; } = [];
}
// When updating, EF adds WHERE RowVersion = @original to the UPDATE
// If another request modified the row, 0 rows are affected โ DbUpdateConcurrencyException
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateProductDto dto,
[FromHeader(Name = "If-Match")] string? etag)
{
var product = await db.Products.FindAsync(id);
if (product == null) return NotFound();
// Validate ETag matches current row version
if (etag != null)
{
var currentEtag = Convert.ToBase64String(product.RowVersion);
if (etag != currentEtag)
return StatusCode(412, "Precondition Failed โ resource was modified");
}
product.Name = dto.Name;
product.Price = dto.Price;
try
{
await db.SaveChangesAsync();
Response.Headers["ETag"] = Convert.ToBase64String(product.RowVersion);
return NoContent();
}
catch (DbUpdateConcurrencyException)
{
return Conflict("The product was modified by another request. Please retry.");
}
}
18 How do you implement an outbox pattern for reliable event publishing in ASP.NET Core? ►
Reliability
// Outbox table โ events stored in the same database transaction as domain changes
public class OutboxMessage
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Type { get; set; } = "";
public string Payload { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? PublishedAt { get; set; } // null = pending
}
// Save domain event to outbox in the SAME transaction as business data
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, OrderDto>
{
public async Task<OrderDto> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = new Order(cmd);
db.Orders.Add(order);
// Write outbox message IN THE SAME TRANSACTION
db.OutboxMessages.Add(new OutboxMessage
{
Type = nameof(OrderPlacedEvent),
Payload = JsonSerializer.Serialize(new OrderPlacedEvent(order.Id, order.CustomerId))
});
await db.SaveChangesAsync(ct); // atomic: order + outbox message
return order.ToDto();
}
}
// Outbox poller โ background service reads and publishes pending messages
public class OutboxPublisher(IServiceScopeFactory factory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using var scope = factory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var bus = scope.ServiceProvider.GetRequiredService<IMessageBus>();
var pending = await db.OutboxMessages
.Where(m => m.PublishedAt == null)
.OrderBy(m => m.CreatedAt)
.Take(100).ToListAsync(ct);
foreach (var msg in pending)
{
await bus.PublishAsync(msg.Type, msg.Payload, ct);
msg.PublishedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync(ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
}
}
19 What is the difference between MassTransit, NServiceBus, and Azure Service Bus in ASP.NET Core? ►
Messaging
- Azure Service Bus โ a managed message broker PaaS (Microsoft Azure). Queues (point-to-point) and Topics/Subscriptions (pub/sub). No broker to manage; pay-per-use. Best for Azure-native workloads. Use the Azure Service Bus .NET SDK directly, or via MassTransit transport.
- MassTransit โ open-source .NET library that provides a consistent, opinionated API over multiple transports: RabbitMQ, Azure Service Bus, Amazon SQS, Kafka, in-memory. Includes saga state machines, request/response, scheduling, and retries. Integrates natively with ASP.NET Core DI and MediatR.
- NServiceBus โ commercial enterprise messaging framework (Particular Software). Most mature and feature-rich; includes sagas, message routing, error management, and monitoring. Higher cost but includes commercial support and the ServicePulse monitoring dashboard.
// MassTransit with RabbitMQ
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<OrderPlacedConsumer>();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host("rabbitmq://localhost", h => { h.Username("guest"); h.Password("guest"); });
cfg.ReceiveEndpoint("order-placed-queue", e =>
{
e.ConfigureConsumer<OrderPlacedConsumer>(ctx);
e.UseMessageRetry(r => r.Intervals(100, 500, 1000)); // retry policy
});
});
});
// Consumer
public class OrderPlacedConsumer(IEmailService email) : IConsumer<OrderPlacedEvent>
{
public async Task Consume(ConsumeContext<OrderPlacedEvent> context)
=> await email.SendConfirmationAsync(context.Message.CustomerId);
}
// Publisher
public class OrdersController(IPublishEndpoint bus) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(Order order)
{
// save order...
await bus.Publish(new OrderPlacedEvent(order.Id, order.CustomerId));
return CreatedAtAction(...);
}
}
20 What is Native AOT in .NET 8 and what does it require from ASP.NET Core APIs? ►
Performance Native AOT (Ahead-of-Time compilation) compiles the entire .NET application to native machine code at publish time. The resulting binary has no JIT compiler, no .NET runtime, and starts in milliseconds โ ideal for serverless and containers.
# Publish as Native AOT
dotnet publish -r linux-x64 -c Release -p:PublishAot=true
# Produces: a single self-contained native binary (no .NET required on the machine)
# Requirements and limitations for ASP.NET Core with Native AOT:
# โ
Minimal APIs (fully AOT-compatible)
# โ
JSON source generation (replace reflection-based JsonSerializer)
# โ
ILogger<T> with LoggerMessage source generator
# โ MVC Controllers with attribute routing (reflection-heavy โ not AOT compatible in .NET 8)
# โ Expression trees (dynamic LINQ, EF LINQ to SQL compiled at runtime)
# โ Dynamic types, Assembly.Load(), reflection on private members
// AOT-compatible minimal API
var builder = WebApplication.CreateSlimBuilder(args); // trimmed hosting model
builder.Services.ConfigureHttpJsonOptions(opts =>
opts.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
var app = builder.Build();
app.MapGet("/products", (AppDbContext db) =>
db.Products.AsNoTracking().ToListAsync());
app.Run();
// JSON context for AOT
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonContext : JsonSerializerContext { }
// The binary is ~10-15MB, starts in <10ms, ideal for:
// - AWS Lambda, Azure Functions (cold start latency)
// - Kubernetes sidecars
// - CLI tools built on .NET
21 How do you implement zero-downtime deployments with ASP.NET Core? ►
Ops
// 1. Graceful shutdown โ complete in-flight requests before stopping
// Program.cs
builder.Services.Configure<HostOptions>(opts =>
opts.ShutdownTimeout = TimeSpan.FromSeconds(30)); // wait up to 30s
// The built-in graceful shutdown stops new requests, waits for current ones to finish
// No code needed โ ASP.NET Core handles SIGTERM automatically
// 2. Health probes for Kubernetes
app.MapGet("/health/live", () => Results.Ok(new { status = "alive" }))
.WithName("HealthLive");
app.MapGet("/health/ready", async (AppDbContext db) =>
{
try
{
await db.Database.ExecuteSqlRawAsync("SELECT 1");
return Results.Ok(new { status = "ready" });
}
catch
{
return Results.ServiceUnavailable(new { status = "not ready" });
}
}).WithName("HealthReady");
// 3. Database migration compatibility โ expand/contract pattern
// Deploy v2 code: Add new_column to DB (nullable, no default required in app)
// Deploy v2 app: App writes to both old_column and new_column
// Run migration: Copy old_column data to new_column
// Deploy v3 app: Read from new_column only
// Deploy v3 migration: Drop old_column
// 4. Feature flags for gradual rollout
app.MapGet("/checkout", async (IFeatureManager fm, [FromServices] CheckoutService svc) =>
{
if (await fm.IsEnabledAsync("NewCheckoutFlow"))
return await svc.NewFlowAsync(); // new code path
return await svc.LegacyFlowAsync(); // old code path
});
// 5. Blue-Green or Rolling deployment via Kubernetes
// Rolling: replace pods one by one (new version + old version run concurrently)
// Blue-Green: run two identical environments, switch traffic instantly
📝 Knowledge Check
These questions mirror real senior-level ASP.NET Core architecture and internals interview scenarios.