📡 Advanced ASP.NET Web API Interview Questions
This lesson targets mid-to-senior .NET backend roles. Topics include JWT authentication, Entity Framework Core, caching, rate limiting, versioning, background services, SignalR, testing with WebApplicationFactory, FluentValidation, AutoMapper, health checks, and logging. These questions separate .NET developers from those who architect production APIs.
Questions & Answers
01 How do you implement JWT authentication in ASP.NET Core Web API? ►
Auth
// Install: dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
// dotnet add package System.IdentityModel.Tokens.Jwt
// Program.cs
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()!;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings.Key)),
ClockSkew = TimeSpan.Zero // no tolerance on expiry
};
});
// Token generation service
public class TokenService(IOptions<JwtSettings> settings)
{
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.Value.Key));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: settings.Value.Issuer,
audience: settings.Value.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(settings.Value.ExpiresMinutes),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
// Protect endpoints
[Authorize] // any authenticated user
[Authorize(Roles = "Admin")] // specific role
[Authorize(Policy = "MinAge18")] // named policy
[AllowAnonymous] // override [Authorize] on controller
02 How do you use Entity Framework Core with ASP.NET Core Web API? ►
Database
// Install: dotnet add package Microsoft.EntityFrameworkCore.SqlServer
// dotnet add package Microsoft.EntityFrameworkCore.Tools
// DbContext
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Product>(e =>
{
e.HasKey(p => p.Id);
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
e.Property(p => p.Price).HasColumnType("decimal(18,2)");
e.HasIndex(p => p.Sku).IsUnique();
});
}
}
// Register in Program.cs
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlServer(builder.Configuration.GetConnectionString("Default"))
.EnableSensitiveDataLogging(builder.Environment.IsDevelopment())
);
// Repository pattern in a controller
[ApiController, Route("api/[controller]")]
public class ProductsController(AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAll()
=> await db.Products.AsNoTracking().ToListAsync();
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var p = await db.Products.FindAsync(id);
return p is null ? NotFound() : p;
}
[HttpPost]
public async Task<ActionResult<Product>> Create(CreateProductDto dto)
{
var product = dto.ToEntity();
db.Products.Add(product);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
// Migrations
// dotnet ef migrations add InitialCreate
// dotnet ef database update
03 What is API versioning in ASP.NET Core and how do you implement it? ►
API Design
// Install: dotnet add package Asp.Versioning.Mvc Asp.Versioning.Mvc.ApiExplorer
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // adds api-supported-versions header
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // /api/v1/products
new HeaderApiVersionReader("X-Api-Version"), // X-Api-Version: 1.0
new QueryStringApiVersionReader("api-version") // ?api-version=1.0
);
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// V1 controller
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new { version = "v1", items = new[] { "A", "B" } });
}
// V2 controller โ new features, different response shape
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
=> Ok(new { version = "v2", data = new[] { "A", "B" }, meta = new { total = 2 } });
}
// Deprecate V1
[ApiVersion("1.0", Deprecated = true)]
public class ProductsV1Controller : ControllerBase { ... }
04 How do you implement caching in ASP.NET Core Web API? ►
Performance
// 1. Response Caching (HTTP cache headers)
builder.Services.AddResponseCaching();
app.UseResponseCaching();
[HttpGet]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public async Task<IActionResult> GetProducts() => Ok(await _svc.GetAllAsync());
// Sets: Cache-Control: public,max-age=60
// 2. In-memory cache
builder.Services.AddMemoryCache();
public class ProductService(IMemoryCache cache, AppDbContext db)
{
public async Task<List<Product>> GetAllAsync()
{
return await cache.GetOrCreateAsync("products", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
entry.SlidingExpiration = TimeSpan.FromMinutes(2);
return await db.Products.AsNoTracking().ToListAsync();
}) ?? [];
}
public async Task InvalidateAsync()
=> cache.Remove("products"); // call after writes
}
// 3. Distributed cache (Redis โ for multi-server deployments)
// Install: dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = builder.Configuration.GetConnectionString("Redis"));
// IDistributedCache usage
public async Task<Product?> GetProductAsync(int id, IDistributedCache cache)
{
var key = $"product:{id}";
var json = await cache.GetStringAsync(key);
if (json != null) return JsonSerializer.Deserialize<Product>(json);
var product = await db.Products.FindAsync(id);
if (product != null)
await cache.SetStringAsync(key, JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
return product;
}
05 What is rate limiting in ASP.NET Core and how do you configure it? ►
Security ASP.NET Core 7+ includes built-in rate limiting via Microsoft.AspNetCore.RateLimiting. No third-party packages required.
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
// Program.cs
builder.Services.AddRateLimiter(options =>
{
// Fixed window: 100 requests per minute per IP
options.AddFixedWindowLimiter("fixed", o =>
{
o.PermitLimit = 100;
o.Window = TimeSpan.FromMinutes(1);
o.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
o.QueueLimit = 0; // no queuing โ reject immediately
});
// Sliding window: smoother distribution
options.AddSlidingWindowLimiter("sliding", o =>
{
o.PermitLimit = 100;
o.Window = TimeSpan.FromMinutes(1);
o.SegmentsPerWindow = 4; // 4 x 15-second segments
});
// Token bucket: burst traffic allowed
options.AddTokenBucketLimiter("token", o =>
{
o.TokenLimit = 200;
o.TokensPerPeriod = 100;
o.ReplenishmentPeriod = TimeSpan.FromMinutes(1);
});
// Concurrency limiter: max simultaneous requests
options.AddConcurrencyLimiter("concurrency", o => o.PermitLimit = 20);
// Partition by user (authenticated users get higher limits)
options.AddPolicy("per-user", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anon",
factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 1000, Window = TimeSpan.FromMinutes(1) }
)
);
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsJsonAsync(
new { error = "Too many requests" }, ct);
};
});
app.UseRateLimiter();
// Apply to controllers/actions
[EnableRateLimiting("fixed")]
public class ApiController : ControllerBase { ... }
[DisableRateLimiting]
[HttpGet("health")]
public IActionResult Health() => Ok();
06 How do you implement background services in ASP.NET Core? ►
Background
// IHostedService โ manual start/stop control
public class StartupCacheWarmer(IProductService svc, ILogger<StartupCacheWarmer> log)
: IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
log.LogInformation("Warming product cache...");
await svc.WarmCacheAsync(ct);
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
// BackgroundService โ long-running loop (abstract base class)
public class OrderProcessingWorker(IServiceScopeFactory factory, ILogger<OrderProcessingWorker> log)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
log.LogInformation("Order processor started");
while (!ct.IsCancellationRequested)
{
using var scope = factory.CreateScope(); // create a scoped DI scope
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
var pending = await orderService.GetPendingAsync(ct);
foreach (var order in pending)
await orderService.ProcessAsync(order, ct);
await Task.Delay(TimeSpan.FromSeconds(30), ct); // wait 30s
}
}
}
// Register in Program.cs
builder.Services.AddHostedService<StartupCacheWarmer>();
builder.Services.AddHostedService<OrderProcessingWorker>();
// Note: BackgroundService runs in the same process as the web app
// For distributed, durable jobs use: Hangfire, Quartz.NET, Azure Service Bus, MassTransit
07 What is SignalR and how do you use it in ASP.NET Core? ►
Realtime SignalR is a library that adds real-time web functionality โ push messages from server to clients over WebSockets (with long-polling fallback). Used for notifications, live dashboards, chat, and collaborative apps.
// Install: dotnet add package Microsoft.AspNetCore.SignalR
// Hub โ defines server-side methods clients can call, and client-side methods server can invoke
public class NotificationHub : Hub
{
// Client calls this: await connection.invoke("SendMessage", "room1", "Hello!")
public async Task SendMessage(string roomId, string message)
{
// Broadcast to group
await Clients.Group(roomId).SendAsync("ReceiveMessage",
new { sender = Context.ConnectionId, message, timestamp = DateTime.UtcNow });
}
// Client joins a group
public async Task JoinRoom(string roomId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomId);
await Clients.Group(roomId).SendAsync("UserJoined", Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception? ex)
{
await Clients.All.SendAsync("UserLeft", Context.ConnectionId);
await base.OnDisconnectedAsync(ex);
}
}
// Program.cs
builder.Services.AddSignalR();
app.MapHub<NotificationHub>("/hubs/notifications");
// Push from a controller or service (inject IHubContext)
public class OrdersController(IHubContext<NotificationHub> hub) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(Order order)
{
var created = await _svc.CreateAsync(order);
// Push real-time notification to all connected clients
await hub.Clients.All.SendAsync("OrderCreated", created);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
}
08 How do you write integration tests for ASP.NET Core Web API? ►
Testing
// Install: dotnet add package Microsoft.AspNetCore.Mvc.Testing
// CustomWebApplicationFactory โ override services for testing
public class CustomWebAppFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace real DB with in-memory for tests
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("TestDb"));
// Seed test data
using var scope = services.BuildServiceProvider().CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
db.Products.AddRange(SeedData.Products);
db.SaveChanges();
});
}
}
// Integration test class
public class ProductsControllerTests(CustomWebAppFactory factory)
: IClassFixture<CustomWebAppFactory>
{
private readonly HttpClient _client = factory.CreateClient();
[Fact]
public async Task GetAll_ReturnsOk_WithProducts()
{
var response = await _client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
var products = await response.Content.ReadFromJsonAsync<List<ProductDto>>();
Assert.NotNull(products);
Assert.NotEmpty(products);
}
[Fact]
public async Task Create_WithValidData_Returns201()
{
var dto = new CreateProductDto { Name = "Test", Price = 9.99m };
var response = await _client.PostAsJsonAsync("/api/products", dto);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
[Fact]
public async Task Create_WithInvalidData_Returns400()
{
var dto = new CreateProductDto { Name = "", Price = -1 }; // invalid
var response = await _client.PostAsJsonAsync("/api/products", dto);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
09 What is FluentValidation and how does it integrate with ASP.NET Core? ►
Validation
// Install: dotnet add package FluentValidation.AspNetCore
// Define a validator
public class CreateProductValidator : AbstractValidator<CreateProductDto>
{
public CreateProductValidator(AppDbContext db)
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.Length(2, 200).WithMessage("Name must be 2-200 characters")
.MustAsync(async (name, ct) =>
!await db.Products.AnyAsync(p => p.Name == name, ct))
.WithMessage("Product name already exists");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be positive")
.LessThan(100_000).WithMessage("Price seems unreasonably high");
RuleFor(x => x.Sku)
.NotEmpty()
.Matches(@"^[A-Z]{3}-\d{4}$").WithMessage("SKU format: ABC-1234");
RuleFor(x => x.Description)
.MaximumLength(2000).When(x => x.Description != null);
}
}
// Register in Program.cs
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// That's it โ validation runs automatically before actions execute
// Invalid requests return 400 with FluentValidation error details
// Manual validation in service layer
public class ProductService(IValidator<CreateProductDto> validator, ...)
{
public async Task<Product> CreateAsync(CreateProductDto dto)
{
var result = await validator.ValidateAsync(dto);
if (!result.IsValid)
throw new ValidationException(result.Errors);
...
}
}
10 What is AutoMapper and how do you use it in ASP.NET Core? ►
Mapping
// Install: dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
// Define mappings in profiles
public class ProductMappingProfile : Profile
{
public ProductMappingProfile()
{
CreateMap<Product, ProductDto>(); // simple โ same property names
CreateMap<CreateProductDto, Product>()
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(_ => DateTime.UtcNow))
.ForMember(dest => dest.Id, opt => opt.Ignore());
// Custom value resolver
CreateMap<Order, OrderDto>()
.ForMember(dest => dest.CustomerName,
opt => opt.MapFrom(src => $"{src.Customer.FirstName} {src.Customer.LastName}"))
.ForMember(dest => dest.ItemCount,
opt => opt.MapFrom(src => src.Items.Count));
// Reverse mapping
CreateMap<ProductDto, Product>().ReverseMap();
}
}
// Register in Program.cs
builder.Services.AddAutoMapper(typeof(Program));
// Use in a controller or service
public class ProductsController(IMapper mapper, AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<ProductDto>>> GetAll()
{
var products = await db.Products.AsNoTracking().ToListAsync();
return mapper.Map<List<ProductDto>>(products);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductDto dto)
{
var product = mapper.Map<Product>(dto);
db.Products.Add(product);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = product.Id },
mapper.Map<ProductDto>(product));
}
}
11 What are health checks in ASP.NET Core and how do you implement them? ►
Ops
// Program.cs
builder.Services.AddHealthChecks()
// Built-in SQL Server health check
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("Default")!,
name: "database",
tags: ["db", "sql"])
// Redis
.AddRedis(
redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
name: "redis",
tags: ["cache"])
// Custom health check
.AddCheck<ExternalApiHealthCheck>("payment-gateway", tags: ["external"]);
// Custom health check
public class ExternalApiHealthCheck(HttpClient http) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct)
{
try
{
var response = await http.GetAsync("/health", ct);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy("Payment gateway is reachable")
: HealthCheckResult.Degraded($"Returned {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Cannot reach payment gateway", ex);
}
}
}
// Map health check endpoints
app.MapHealthChecks("/health/live"); // liveness โ is app running?
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("db"), // readiness โ can handle traffic?
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse // JSON format
});
app.MapHealthChecks("/health/full",
new HealthCheckOptions { Predicate = _ => true }); // all checks
12 How do you implement structured logging in ASP.NET Core with Serilog? ►
Logging
// Install: dotnet add package Serilog.AspNetCore Serilog.Sinks.Seq Serilog.Sinks.Console
// Program.cs
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.Seq("http://localhost:5341") // Seq log server
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30)
.CreateLogger();
builder.Host.UseSerilog();
// Use in a controller (ILogger<T> from DI)
public class OrdersController(ILogger<OrdersController> logger) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(CreateOrderDto dto)
{
// Structured logging โ properties are searchable in Seq/ELK/Datadog
logger.LogInformation("Creating order for customer {CustomerId}, total {Total}",
dto.CustomerId, dto.Total);
var order = await _svc.CreateAsync(dto);
logger.LogInformation("Order {OrderId} created successfully", order.Id);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
}
// Request logging middleware
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
options.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
{
diagCtx.Set("UserId", httpCtx.User.FindFirstValue(ClaimTypes.NameIdentifier));
diagCtx.Set("ClientIp", httpCtx.Connection.RemoteIpAddress);
};
});
13 What is the Repository and Unit of Work pattern with EF Core? ►
Architecture
// Generic repository interface
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id, CancellationToken ct = default);
Task<List<T>> GetAllAsync(CancellationToken ct = default);
void Add(T entity);
void Update(T entity);
void Remove(T entity);
}
// Generic EF Core repository
public class EfRepository<T>(AppDbContext db) : IRepository<T> where T : class
{
protected readonly DbSet<T> _set = db.Set<T>();
public Task<T?> GetByIdAsync(int id, CancellationToken ct = default)
=> _set.FindAsync([id], ct).AsTask();
public Task<List<T>> GetAllAsync(CancellationToken ct = default)
=> _set.AsNoTracking().ToListAsync(ct);
public void Add(T e) => _set.Add(e);
public void Update(T e) => db.Entry(e).State = EntityState.Modified;
public void Remove(T e) => _set.Remove(e);
}
// Unit of Work โ one SaveChanges across multiple repos
public interface IUnitOfWork : IDisposable
{
IRepository<Product> Products { get; }
IRepository<Order> Orders { get; }
Task<int> CommitAsync(CancellationToken ct = default);
}
public class UnitOfWork(AppDbContext db) : IUnitOfWork
{
public IRepository<Product> Products { get; } = new EfRepository<Product>(db);
public IRepository<Order> Orders { get; } = new EfRepository<Order>(db);
public Task<int> CommitAsync(CancellationToken ct = default)
=> db.SaveChangesAsync(ct);
public void Dispose() => db.Dispose();
}
// Controller using UoW
public class OrdersController(IUnitOfWork uow) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(CreateOrderDto dto)
{
var order = new Order(dto);
uow.Orders.Add(order);
await uow.CommitAsync(); // single SaveChanges
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
}
14 What is IHttpClientFactory and why should you use it? ►
HTTP IHttpClientFactory manages the lifecycle of HttpMessageHandler objects, solving two classic problems: socket exhaustion (from creating new HttpClient per request) and stale DNS (from reusing the same HttpClient forever).
// Program.cs โ register named or typed clients
// Named client
builder.Services.AddHttpClient("PaymentApi", client =>
{
client.BaseAddress = new Uri("https://payment.api.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(10);
}).AddTransientHttpErrorPolicy(p => // Polly retry
p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1)));
// Typed client โ strongly typed wrapper
public class PaymentServiceClient(HttpClient http)
{
public async Task<PaymentResult> ChargeAsync(ChargeRequest req)
{
var response = await http.PostAsJsonAsync("/charge", req);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>()!;
}
}
builder.Services.AddHttpClient<PaymentServiceClient>(client =>
client.BaseAddress = new Uri("https://payment.api.com"));
// Use in controller (typed client injected directly)
public class OrdersController(PaymentServiceClient payments) : ControllerBase
{
[HttpPost("{id}/pay")]
public async Task<IActionResult> Pay(int id, ChargeRequest req)
{
var result = await payments.ChargeAsync(req);
return Ok(result);
}
}
// Use named client
public class MyService(IHttpClientFactory factory)
{
public async Task<T> GetAsync<T>(string path)
{
var client = factory.CreateClient("PaymentApi");
var response = await client.GetAsync(path);
return await response.Content.ReadFromJsonAsync<T>()!;
}
}
15 What is the CQRS pattern and how do you implement it with MediatR? ►
Architecture CQRS (Command Query Responsibility Segregation) separates read operations (Queries) from write operations (Commands). MediatR is a popular in-process mediator library that implements this pattern cleanly in .NET.
// Install: dotnet add package MediatR
// Command โ write operation
public record CreateProductCommand(string Name, decimal Price) : IRequest<ProductDto>;
// Command handler
public class CreateProductHandler(AppDbContext db, IMapper mapper)
: IRequestHandler<CreateProductCommand, ProductDto>
{
public async Task<ProductDto> Handle(CreateProductCommand cmd, CancellationToken ct)
{
var product = new Product { Name = cmd.Name, Price = cmd.Price };
db.Products.Add(product);
await db.SaveChangesAsync(ct);
return mapper.Map<ProductDto>(product);
}
}
// Query โ read operation
public record GetProductByIdQuery(int Id) : IRequest<ProductDto?>;
public class GetProductByIdHandler(AppDbContext db, IMapper mapper)
: IRequestHandler<GetProductByIdQuery, ProductDto?>
{
public async Task<ProductDto?> Handle(GetProductByIdQuery query, CancellationToken ct)
{
var product = await db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == query.Id, ct);
return product == null ? null : mapper.Map<ProductDto>(product);
}
}
// Register in Program.cs
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
// Thin controller โ just dispatches commands/queries
public class ProductsController(IMediator mediator) : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetById(int id)
{
var dto = await mediator.Send(new GetProductByIdQuery(id));
return dto == null ? NotFound() : Ok(dto);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductCommand cmd)
{
var dto = await mediator.Send(cmd);
return CreatedAtAction(nameof(GetById), new { id = dto.Id }, dto);
}
}
16 How do you implement the Options pattern and configuration validation? ►
Config
// Settings class with Data Annotations validation
public class DatabaseSettings
{
public const string SectionName = "Database";
[Required]
public string ConnectionString { get; set; } = "";
[Range(1, 100)]
public int MaxConnections { get; set; } = 20;
[Range(0, 3600)]
public int CommandTimeoutSeconds { get; set; } = 30;
}
// Program.cs โ register with eager validation
builder.Services.AddOptions<DatabaseSettings>()
.BindConfiguration(DatabaseSettings.SectionName)
.ValidateDataAnnotations() // validate on first use
.ValidateOnStart(); // validate at startup (throw if invalid)
// Three ways to consume options:
// 1. IOptions<T> โ singleton snapshot (value doesn't change)
public class QueryService(IOptions<DatabaseSettings> opts)
{
private readonly DatabaseSettings _db = opts.Value;
}
// 2. IOptionsSnapshot<T> โ per-request snapshot (scoped โ reflects config changes)
public class PerRequestService(IOptionsSnapshot<DatabaseSettings> opts)
{
private readonly DatabaseSettings _db = opts.Value;
}
// 3. IOptionsMonitor<T> โ real-time (changes reflected immediately)
public class MonitoredService(IOptionsMonitor<DatabaseSettings> monitor)
{
private DatabaseSettings Current => monitor.CurrentValue;
public MonitoredService(IOptionsMonitor<DatabaseSettings> monitor)
{
monitor.OnChange(settings => Log.Information("Settings changed"));
}
}
17 What is output caching in ASP.NET Core 7+? ►
Performance Output caching (introduced in ASP.NET Core 7) caches the entire HTTP response at the server side. Unlike response caching (which relies on client/proxy Cache-Control headers), output caching is fully server-controlled and supports cache invalidation tags.
// Program.cs
builder.Services.AddOutputCache(options =>
{
// Named policies
options.AddPolicy("Default", builder =>
builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("ByUser", builder =>
builder.Expire(TimeSpan.FromMinutes(1))
.VaryByValue(ctx => new KeyValuePair<string, string>(
"UserId", ctx.Request.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "")));
options.AddPolicy("NoCache", builder => builder.NoCache());
});
app.UseOutputCache();
// Apply to actions
[HttpGet]
[OutputCache(PolicyName = "Default")] // 5 min cache
public async Task<IActionResult> GetProducts() => Ok(await _svc.GetAllAsync());
[HttpGet("{id}")]
[OutputCache(Duration = 60, VaryByRouteValueNames = ["id"])] // vary by route param
public async Task<IActionResult> GetById(int id) => Ok(await _svc.GetByIdAsync(id));
// Cache tags โ enable targeted invalidation
[HttpGet]
[OutputCache(Tags = ["products"], Duration = 300)]
public IActionResult GetAll() => Ok(_svc.GetAll());
// Invalidate by tag when data changes
public class ProductService(IOutputCacheStore cache)
{
public async Task UpdateAsync(Product product)
{
await _db.SaveChangesAsync();
await cache.EvictByTagAsync("products", default); // bust the cache
}
}
18 How do you implement authorization policies and claims-based auth? ►
Auth
// Program.cs โ define policies
builder.Services.AddAuthorization(options =>
{
// Simple role policy
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
// Claim-based policy
options.AddPolicy("VerifiedEmail", policy =>
policy.RequireClaim("email_verified", "true"));
// Custom requirement policy
options.AddPolicy("MinAge18", policy =>
policy.AddRequirements(new MinAgeRequirement(18)));
// Combine requirements
options.AddPolicy("SeniorAdmin", policy =>
policy.RequireRole("Admin")
.RequireClaim("tenure_years")
.RequireAssertion(ctx =>
int.TryParse(ctx.User.FindFirstValue("tenure_years"), out var y) && y >= 5));
});
// Custom requirement and handler
public record MinAgeRequirement(int MinAge) : IAuthorizationRequirement;
public class MinAgeHandler : AuthorizationHandler<MinAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinAgeRequirement requirement)
{
var dob = context.User.FindFirstValue("date_of_birth");
if (dob != null && DateTime.TryParse(dob, out var birthDate))
{
var age = DateTime.Today.Year - birthDate.Year;
if (age >= requirement.MinAge)
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
builder.Services.AddSingleton<IAuthorizationHandler, MinAgeHandler>();
// Apply to controllers/actions
[Authorize(Policy = "AdminOnly")]
[Authorize(Policy = "VerifiedEmail")] // both must pass
public class AdminController : ControllerBase { ... }
[Authorize(Policy = "MinAge18")]
[HttpPost("checkout")]
public IActionResult Checkout() { ... }
19 What is Polly and how do you use resilience policies with ASP.NET Core? ►
Resilience
// .NET 8+ โ Microsoft.Extensions.Http.Resilience (built on Polly v8)
// Install: dotnet add package Microsoft.Extensions.Http.Resilience
// Program.cs
builder.Services.AddHttpClient<PaymentClient>(client =>
client.BaseAddress = new Uri("https://payment.api.com"))
.AddStandardResilienceHandler(options =>
{
// Retry (3 times, exponential backoff)
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
// Circuit breaker
options.CircuitBreaker.FailureRatio = 0.5; // open after 50% failure
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
// Total timeout
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);
});
// Polly v7 (older projects):
builder.Services.AddHttpClient<PaymentClient>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))))
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)));
// Handle circuit breaker open in controller
[HttpPost("{id}/charge")]
public async Task<IActionResult> Charge(int id, ChargeRequest req)
{
try
{
var result = await _payment.ChargeAsync(req);
return Ok(result);
}
catch (BrokenCircuitException)
{
return StatusCode(503, "Payment service temporarily unavailable");
}
}
20 How do you implement file uploads and handle large files in ASP.NET Core? ►
Files
// Standard file upload (buffered โ entire file in memory)
[HttpPost("upload")]
[RequestSizeLimit(50_000_000)] // 50MB limit
public async Task<IActionResult> Upload(IFormFile file)
{
if (file.Length == 0) return BadRequest("Empty file");
if (!AllowedTypes.Contains(file.ContentType)) return BadRequest("Invalid type");
var filename = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var savePath = Path.Combine("uploads", filename);
await using var stream = System.IO.File.Create(savePath);
await file.CopyToAsync(stream);
return Ok(new { url = $"/files/{filename}" });
}
// Large file upload (streamed โ NOT buffered in memory)
// Disable form buffering for large files
[HttpPost("upload/stream")]
[DisableFormValueModelBinding]
[RequestSizeLimit(long.MaxValue)]
public async Task<IActionResult> UploadStream()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
return BadRequest("Not multipart");
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType), 70);
var reader = new MultipartReader(boundary, Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasDisposition = ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var disposition);
if (hasDisposition && disposition!.IsFileDisposition())
{
var filename = disposition.FileName.Value!;
var path = Path.Combine("uploads", filename);
await using var fileStream = System.IO.File.Create(path);
await section.Body.CopyToAsync(fileStream); // stream to disk โ no memory bloat
}
section = await reader.ReadNextSectionAsync();
}
return Ok();
}
// Upload to Azure Blob Storage
// Install: dotnet add package Azure.Storage.Blobs
public async Task<string> UploadToBlobAsync(IFormFile file, BlobContainerClient container)
{
var blobName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var blobClient = container.GetBlobClient(blobName);
await blobClient.UploadAsync(file.OpenReadStream(), overwrite: true);
return blobClient.Uri.AbsoluteUri;
}
21 What is the difference between synchronous and asynchronous action methods? ►
Async
- Synchronous actions โ block a thread for the entire duration of the operation (including I/O waits). Under high load, all threads can be exhausted โ the server stops accepting new requests even though most threads are idle waiting for I/O.
- Asynchronous actions โ release the thread during I/O operations. The thread pool thread is free to handle other requests while waiting for database queries, HTTP calls, or file reads. Dramatically improves throughput under load with no code complexity penalty.
// โ Synchronous โ blocks a thread during the database call
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var product = _db.Products.FirstOrDefault(p => p.Id == id); // blocks thread
return product == null ? NotFound() : product;
}
// โ
Asynchronous โ releases thread while DB query runs
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetByIdAsync(int id)
{
var product = await _db.Products.FirstOrDefaultAsync(p => p.Id == id); // non-blocking
return product == null ? NotFound() : product;
}
// โ
Parallel async calls (independent operations)
[HttpGet("dashboard")]
public async Task<IActionResult> Dashboard()
{
var (orders, products, users) = await (
_orders.GetRecentAsync(),
_products.GetLowStockAsync(),
_users.GetActiveCountAsync()
).WhenAll(); // all run simultaneously
return Ok(new { orders, products, users });
}
// Rule: always use async for I/O-bound operations (DB, HTTP, file system)
// CPU-bound operations don't benefit from async โ use Task.Run for offloading
📝 Knowledge Check
Test your understanding of advanced ASP.NET Core Web API patterns and features.