🎇Beyond Basics: Top 10 .NET Questions for Mid to Senior Developers

Read this article on Medium.

Navigating a .NET interview can feel like walking a tightrope between theory and real-world experience. Mid-level developers are expected to demonstrate solid understanding of C#design principles, and frameworks, while senior developers must show judgment in architecturepatterns, and performance.

This guide covers questions that go beyond the basics â€” spanning SOLID principlesdependency injection, common design patternsBlazorEntity Frameworkcaching, and performance management. You’ll get both conceptual questions and practical scenarios, helping you prepare for interviews that test not just knowledge, but how you think as a .NET developer.

Whether you’re brushing up for a technical screen or preparing for a senior-level discussion, these questions are designed to challenge your understanding and showcase your expertise.

1️⃣SOLID Principles: The Backbone of Clean .NET Code

SOLID isn’t just a buzzword, it’s a roadmap for maintainabletestable, and scalable code. Understanding the principles is one thing; knowing when and how to apply them is another. Let’s break it down.

Mid-Level Questions

These focus on comprehension and practical application:

Single Responsibility Principle (SRP)

✔️ What is SRP, and why is it important?
SRP states that a class should have only one reason to change, meaning it should have a single responsibility. This makes code easier to maintaintest, and understand.

✔️ Can you give an example of a class that violates SRP and how you would refactor it?
To refactor, split into separate classes: InvoiceCalculatorInvoiceRepository, and InvoiceNotifier.

class InvoiceProcessor
{
public void CalculateTotal() { /*...*/ }
public void SaveToDatabase() { /*...*/ } // Violates SRP
public void SendEmailConfirmation() { /*...*/ } // Violates SRP
}

Senior-Level Considerations

✅Practical judgment: Splitting classes for SRP is good, but don’t over-split. Too many tiny classes can make code harder to navigate.

✅Legacy code: Sometimes a single class may have multiple responsibilities; refactor incrementally rather than rewriting everything at once.

✅Guiding question: â€œDoes this class have more than one reason to change?” If yes, consider refactoring; if it adds unnecessary complexity, it may be okay as-is.

Open/Closed Principle (OCP)

✔️ How does OCP encourage extensibility without modifying existing code?
OCP states that software entities should be open for extension, closed for modification. You can add new behavior by extending classes or implementing interfaces, without changing existing tested code.

✔️ Provide an example of using interfaces or inheritance to follow OCP.

  • Open for extension: You can introduce new discount strategies (VIPDiscount, HolidayDiscount, etc.) by implementing IDiscountStrategy.
  • Closed for modification: Order and the existing discounts don’t need any changes. They are safe, tested, and reliable.
interface IDiscountStrategy { decimal Apply(decimal total); }

class SeasonalDiscount : IDiscountStrategy { /*...*/ }
class ClearanceDiscount : IDiscountStrategy { /*...*/ }

class Order
{
public decimal CalculateTotal(IDiscountStrategy discount) => discount.Apply(100);
}

Senior-Level Considerations

✅When to apply: Great for code that frequently changes, like plugin systems, payment processors, or discount strategies.

✅Overuse risk: Creating interfaces for every class or tiny feature can lead to excessive abstraction and maintenance overhead.

✅Guiding question: â€œWill this module need future extensions?” If not, don’t over-engineer.

Liskov Substitution Principle (LSP)

✔️ What does LSP mean in practice?
Subtypes 
must be substitutable for their base types without altering expected behavior. Any code using the base class should work with subclasses seamlessly.

✔️ How can violating LSP lead to runtime bugs?

Violating LSP can cause runtime errors because a subclass (like Ostrich) doesn’t fully support the behavior expected from its base class (Bird), breaking code that relies on that contract.

Using Ostrich where Bird is expected will break the program at runtime.

class Bird { public virtual void Fly() { /*...*/ } }
class Ostrich : Bird { public override void Fly() { throw new NotSupportedException(); } } // Violates LSP

Senior-Level Considerations

✅Practical judgment: Always ensure subclasses honor expected behavior. If a subclass requires changing client code to use it, LSP is violated.

✅Common senior scenario: Refactoring legacy hierarchies — sometimes it’s better to extract shared behavior into a new interface or service rather than forcing inheritance.

✅Guiding question: â€œCan I replace the base class with this subclass everywhere without breaking anything?”

Interface Segregation Principle (ISP)

✔️Why avoid “fat” interfaces?
Large interfaces force classes to implement methods they don’t need, creating unnecessary dependencies.

✔️How would you refactor a large interface into smaller, more focused ones?
Classes implement only the interfaces relevant to them.

interface IWorker { void Work(); void Eat(); } // Fat interface

interface IWorkable { void Work(); }
interface IFeedable { void Eat(); } // Segregated interfaces

Senior-Level Considerations

✅When to apply: Useful when building frameworks, reusable libraries, or systems with varied consumer requirements.

✅Overuse risk: Avoid creating tiny interfaces for every single method — it can lead to interface explosion and confusion.

✅Guiding question: â€œDoes this interface force implementers to depend on methods they don’t need?” If yes, split it carefully.

Dependency Inversion Principle (DIP)

✔️How does DIP relate to dependency injection in .NET?
DIP states that high-level modules should not depend on low-level modules, rather they should depend on abstractions (interfaces or abstract classes). Dependency Injection provides these abstractions at runtime.

✔️When would you use an abstract class vs an interface to follow DIP?

  • Use interfaces for multiple implementations or pure contracts.
  • Use abstract classes if you want shared behavior or partial implementation.
public interface ILogger { void Log(string message); }
public class ConsoleLogger : ILogger { public void Log(string msg) => Console.WriteLine(msg); }

public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger) { _logger = logger; }
public void Process() { _logger.Log("Order processed."); }
}

Senior-Level Considerations

✅When to apply: Essential in large systems for testability, decoupling, and dependency injection.

✅Overuse risk: Don’t abstract everything. Over-abstraction for simple classes adds complexity without benefit.

✅Guiding question: â€œDoes this high-level module really need to know the concrete implementation, or is an abstraction enough?”

Senior developers need to think beyond “what” SOLID is.
They consider 
when and how to use it effectively:

✅Trade-offs and judgment: Over-application of SOLID can lead to unnecessary abstractions and excessive classes. A senior dev knows when a single class handling multiple responsibilities is acceptable for simplicity and maintainability.

✅Architecture impact: SOLID guides the structure of larger systems. Using it well affects API design, modularization, and maintainability.

✅Refactoring strategy: Seniors often identify SRP or DIP violations in legacy code and plan phased refactoring rather than rewriting everything at once.

Always ask yourself: “Does this change make the code easier to read, test, or extend?”
If not, it may be over-engineered.

2️⃣Dependency Injection: Decoupling Made Practical

Dependency Injection is more than a buzzword. It’s a key tool in building testablemaintainable .NET applications. Understanding how it works, why it matters, and when to apply it separates mid-level knowledge from senior judgment.

Mid-Level Questions

✔️What is Dependency Injection?
DI is a design pattern where a class receives its dependencies from the outside rather than creating them internally. This promotes decoupling and makes testing easier.

️️✔️What are the common types of DI in .NET?

  • Constructor injection (most common): dependencies are passed via constructor.
  • Property injection: dependencies are set via public properties.
  • Method injection: dependencies are passed into methods.

✔️How do you register services in .NET?
Use the built-in IServiceCollection in Program.cs or Startup.cs:

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
builder.Services.AddTransient<EmailNotifier>();

✔️How do you consume injected services in a class?

  • Constructor injection: Preferred for required dependencies and ensures immutability.
  • Property injection: Useful for optional dependencies, but must be set before use.
  • Method injection: Good for dependencies that are only needed for a single operation.
// Service interface
public interface IOrderService
{
void Process();
}

// Concrete implementation
public class OrderService : IOrderService
{
public void Process() => Console.WriteLine("Order processed!");
}

// 1. Constructor Injection (most common)
public class OrderControllerConstructor
{
private readonly IOrderService _orderService;

public OrderControllerConstructor(IOrderService orderService)
{
_orderService = orderService;
}

public void ProcessOrder() => _orderService.Process();
}

// 2. Property Injection
public class OrderControllerProperty
{
public IOrderService OrderService { get; set; } // Must be set externally

public void ProcessOrder() => OrderService.Process();
}

// 3. Method Injection
public class OrderControllerMethod
{
public void ProcessOrder(IOrderService orderService) => orderService.Process();
}

// Usage example
var orderService = new OrderService();

// Constructor injection
var controller1 = new OrderControllerConstructor(orderService);
controller1.ProcessOrder();

// Property injection
var controller2 = new OrderControllerProperty { OrderService = orderService };
controller2.ProcessOrder();

// Method injection
var controller3 = new OrderControllerMethod();
controller3.ProcessOrder(orderService);

✔️Why is DI important in Blazor or ASP.NET Core?
DI 
allows componentsservices, and controllers to remain loosely coupled and easily testable, supporting scoped lifetimessingleton services, and efficient resource management.

Senior-Level Considerations

✅Lifetime management:

  • Transient: New instance every time (good for lightweight, stateless services).
  • Scoped: One instance per request (ideal for web requests or Blazor SSR pages).
  • Singleton: One instance for the app lifetime (use carefully — watch thread safety).
var builder = WebApplication.CreateBuilder(args);

// 1. Register services
builder.Services.AddTransient<IOrderService, OrderService>(); // Transient
builder.Services.AddScoped<IOrderService, OrderService>(); // Scoped
builder.Services.AddSingleton<IOrderService, OrderService>(); // Singleton

var app = builder.Build();

// Example usage:

var transientService = app.Services.GetRequiredService<IOrderService>();
var scopedService = app.Services.GetRequiredService<IOrderService>();
var singletonService = app.Services.GetRequiredService<IOrderService>();

// Using Constructor Injection
var controllerConstructor = new OrderControllerConstructor(transientService);
controllerConstructor.ProcessOrder();

// Using Property Injection
var controllerProperty = new OrderControllerProperty { OrderService = scopedService };
controllerProperty.ProcessOrder();

// Using Method Injection
var controllerMethod = new OrderControllerMethod();
controllerMethod.ProcessOrder(singletonService);

✅Avoid over-abstraction:

  • Not every class needs an interface. Excessive DI can lead to unnecessary complexity.
  • Ask: “Will this class ever have multiple implementations?”

✅Legacy code integration:

  • Inject services incrementally rather than rewriting everything.
  • Use adapters or facades to integrate non-DI-friendly components.

✅Testing and mocking:

  • DI makes unit testing easier because dependencies can be swapped with mocks or fakes.
  • Senior devs design services with testability in mind without overengineering.

✅DI in Blazor context:

  • Blazor Server: Scoped services persist per user connection.
  • Blazor WebAssembly: Singleton and transient lifetimes behave differently; be mindful of app-wide state.

DI is a tool, not a rule.
The best developers know 
when to inject,
when to instantiate, and when to refactor.

3️⃣MediatR, CQRS & Vertical Slice Architecture

Modern .NET architecture often leans on MediatRCQRS, and Vertical Slice Architecture to keep code cleanmaintainable, and testable. These patterns separate concernsreduce coupling, and make large applications easier to read and maintain.

Mid-Level Questions & Answers

✔️What is CQRS and why use it?

  • CQRS (Command Query Responsibility Segregation) separates read operations (queries) from write operations (commands).
  • Query: Returns data without modifying state
  • Command: Performs actions that change state
    Benefit: Optimizes for scalability, simplifies complex business logic, and makes intent clear.

✔️What is MediatR in .NET?
MediatR is a library that implements the mediator pattern, decoupling senders (controllersservices) from handlers. Instead of calling services directly, you send commands/queries to MediatR, which routes them to the correct handler.

// Example: Query
public class GetOrdersQuery : IRequest<List<Order>> { }
public class GetOrdersQueryHandler : IRequestHandler<GetOrdersQuery, List<Order>>
{
private readonly DbContext _context;
public GetOrdersQueryHandler(DbContext context) => _context = context;
public Task<List<Order>> Handle(GetOrdersQuery request, CancellationToken ct)
=> _context.Orders.ToListAsync(ct);
}

// Usage
var orders = await mediator.Send(new GetOrdersQuery());

✔️What is Vertical Slice Architecture?
Instead of layering by technical concerns (Controllers â†’ Services â†’ Repositories), Vertical Slice organizes code by feature/endpoint. Each slice contains everything needed for that featurecommand/queryhandlervalidationmapping.

Benefit: Reduces coupling, makes features easier to maintain, and aligns perfectly with MediatR/CQRS.

Senior-Level Considerations

✅When to use CQRS

  • Great for complex domains or systems with high read/write load differences.
  • Avoid over-engineering: simple CRUD apps often don’t need full CQRS.

✅Trade-offs of MediatR

  • Pros: Decouples components, enforces single responsibility per handler, easier testing.
  • Cons: Can lead to too many small classes, making navigation harder; may add slight latency due to additional indirection.

✅Vertical Slice strategy

  • Pro: Makes features self-contained, which reduces cross-feature dependency.
  • Con: Can seem unfamiliar at first; developers need discipline for naming, folders, and consistency.

✅Design judgment questions

  • Should a query return DTOs directly or domain entities?
    Generally, returning DTOs prevents exposing internal domain models and keeps your API stable. Use domain entities only when internal consistency and behavior are required.
  • How to handle transactional operations across multiple slices?
    Consider using a unit-of-work or domain events to coordinate actions, while keeping slices loosely coupled to support maintainability.
  • When to combine slices versus keep them isolated?
    Combine slices if the operations are highly interdependent and performancecritical; otherwise, isolation improves testability and scalability.

Senior developers don’t just implement CQRS or Vertical Slice, they evaluate when it simplifies maintenance and scalability and when it’s unnecessary abstraction.

4️⃣Design Patterns That Actually Matter

Knowing patterns isn’t just about memorizing names. It’s about recognizing where to apply them and understanding trade-offs. Here’s a breakdown of the ones that come up most often for mid-to-senior .NET roles.

Mid-Level Questions & Answers

✔️What is the Factory Pattern?

The Factory Pattern provides an interface for creating objects without specifying the concrete class. Useful for abstracting object creation, especially when the type isn’t known until runtime.

For example, you have a payment system that supports multiple providers:

// Product interface
public interface IPaymentProcessor
{
void Pay(decimal amount);
}

// Concrete products
public class PayPalProcessor : IPaymentProcessor
{
public void Pay(decimal amount) => Console.WriteLine($"PayPal processed {amount:C}");
}

public class StripeProcessor : IPaymentProcessor
{
public void Pay(decimal amount) => Console.WriteLine($"Stripe processed {amount:C}");
}

// Factory
public static class PaymentFactory
{
public static IPaymentProcessor Create(string provider)
{
return provider switch
{
"PayPal" => new PayPalProcessor(),
"Stripe" => new StripeProcessor(),
_ => throw new NotSupportedException("Payment provider not supported")
};
}
}

IPaymentProcessor interface

  • Defines the common behavior: any payment processor must implement Pay().
  • Client code only relies on this interface, not specific processors.
  • Concrete processors (PayPalProcessor, StripeProcessor)
  • Implement IPaymentProcessor in different ways.
  • They are independent of the client; adding a new one later won’t break existing code.

PaymentFactory

  • Handles the decision of which concrete processor to create.
  • Client doesn’t need to know the class names or how to construct them.
  • Centralizes object creation in one place.

✔️What is the Strategy Pattern?

  • Encapsulates behaviors or algorithms: Instead of putting multiple “if/else” or “switch” logic inside a class, each behavior goes into its own class.
  • Makes behaviors interchangeable at runtime: You can swap out one behavior for another without changing the client code.
  • Keeps the code open for extension but closed for modification (OCP).

For example, you have a checkout system where different discounts apply:

interface IDiscountStrategy { decimal Apply(decimal amount); }
class SeasonalDiscount : IDiscountStrategy { public decimal Apply(decimal amount) => amount * 0.9M; }
class ClearanceDiscount : IDiscountStrategy { public decimal Apply(decimal amount) => amount * 0.5M; }

class Checkout
{
private readonly IDiscountStrategy _strategy;
public Checkout(IDiscountStrategy strategy) => _strategy = strategy;
public decimal Total(decimal amount) => _strategy.Apply(amount);
}
  • IDiscountStrategy defines the behavior contract.
  • SeasonalDiscount and ClearanceDiscount implement the behavior differently.
  • Checkout doesn’t care which discount is applied—it just calls _strategy.Apply(amount).

✔️What is the Observer Pattern?

The Observer Pattern allows objects to subscribe and react to events in other objects. Useful for event-driven programmingnotifications, or Blazor state updates.

For example, this code implements a simple Observer Pattern, where Observer instances can subscribe to a Subject and are automatically notified (via OnChange) whenever the subject calls Notify().

class Subject
{
public event Action? OnChange;
public void Notify() => OnChange?.Invoke();
}

class Observer
{
public void Subscribe(Subject s) => s.OnChange += () => Console.WriteLine("Changed!");
}

Subject class

  • Maintains an event (OnChange) to notify observers.
  • Notify() triggers the event, which calls all subscribed observers.

Observer class

  • Subscribes to the subject’s event using Subscribe().
  • When Notify() is called, the observer reacts by printing "Changed!".

✔️What is the BFF (Backend For Frontend) pattern?

Tailors backend endpoints for specific frontends (mobile, web) to reduce over-fetching, simplify logic, and optimize performance.

Suppose you have a single backend that serves both Web and Mobile clients.

  • Web client needs: Id, Name, Email, Orders, CartItems
  • Mobile client needs: Id, Name, Orders only (smaller payload to save bandwidth)

A single API returning all data would over-fetch for mobile users. A BFF solves this by creating frontend-specific endpoints.

// Shared domain model
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public List<Order> Orders { get; set; } = new();
public List<CartItem> CartItems { get; set; } = new();
}

public class Order { public int Id { get; set; } public decimal Total { get; set; } }
public class CartItem { public int Id { get; set; } public string Product { get; set; } = string.Empty; }

Web BFF Endpoint

[ApiController]
[Route("api/web/user")]
public class WebUserController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = UserRepository.GetUser(id);
// Return full DTO for web
var dto = new
{
user.Id,
user.Name,
user.Email,
user.Orders,
user.CartItems
};
return Ok(dto);
}
}

Mobile BFF Endpoint

[ApiController]
[Route("api/mobile/user")]
public class MobileUserController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var user = UserRepository.GetUser(id);
// Return a smaller DTO for mobile
var dto = new
{
user.Id,
user.Name,
user.Orders
};
return Ok(dto);
}
}
  • Reduces over-fetching — Mobile only gets the data it needs.
  • Simplifies frontend logic — Web and Mobile each get exactly the shape of data they expect.
  • Optimizes performance — Smaller payloads for mobile improve speed and reduce bandwidth.

✔️CQRS + Mediator (covered in previous section)

Recognize it as both a pattern and an architectural approach, not just a library.

Senior-Level Considerations

✅When to apply patterns

  • Patterns are tools, not rules.
  • Ask: “Does this pattern simplify the design or add unnecessary complexity?”

✅Trade-offs

  • Factory/Strategy: Overuse can lead to too many small classes. Combine logically where appropriate.
  • Observer: Can introduce unexpected side effects; use with clear contracts.
  • BFF: Adds an extra layer; only use when multiple frontends benefit.

✅Pattern selection in .NET

  • Favor built-in abstractions and libraries when available (e.g., IServiceProvider + DI can replace simple factories).
  • Patterns shine most when solving recurrent problems in large, evolving systems.

Senior developers don’t just know patterns — they identify which problem each pattern solves and when it’s overkill.

5️⃣Entity Framework Core — Performance & Pitfalls

EF Core is a powerful ORM, but even experienced developers can fall into performance traps. Interviewers want to see that you understand how EF works under the hood, and can optimize queries and avoid common mistakes.

Mid-Level Questions & Answers

✔️What is Lazy Loading vs Eager Loading?

  • Lazy Loading: Related entities are loaded only when accessed. Can lead to N+1 query problem if not careful.
  • Eager Loading: Related entities are loaded upfront using .Include(). Reduces multiple trips to the database.

Only load what you need, when you need it.
If you’re not sure you’ll use related data → lazy load.
If you know you need it all → 
eager load.

// Eager Loading example
var orders = context.Orders.Include(o => o.Customer).ToList();

✔️What is the N+1 problem?

  • Occurs when a query triggers additional queries per entity, leading to performance degradation.
  • Avoid by using Include, Select, or projection into DTOs.
// Get all blogs
var blogs = context.Blogs.ToList();

// Access related posts
foreach (var blog in blogs)
{
Console.WriteLine($"Blog {blog.Name} has {blog.Posts.Count} posts");
}

What happens under the hood:

  • Query 1: SELECT * FROM Blogs → loads all blogs
  • Query 2…N: For each blog, SELECT * FROM Posts WHERE BlogId = ... → one query per blog
  • If you have 100 blogs, that’s 1 + 100 = 101 queries → major performance hit.

Refactor Example using Eager Loading with Include
Generates one query with a join, avoiding multiple trips to the database.

var blogs = context.Blogs
.Include(b => b.Posts) // Load posts in the same query
.ToList();

Refactor Example using Projection into DTOs
Only loads the data you actually need, still in one query.

var blogs = context.Blogs
.Select(b => new
{
b.Id,
b.Name,
PostCount = b.Posts.Count
})
.ToList();

The N+1 problem occurs whenever a collection or navigation property triggers one query per entity.
Avoid it by 
eager loading (Include) or projecting only what you need.

✔️Tracking vs No-Tracking Queries

  • Tracking (default): EF keeps track of entities for change detection.
  • No-Tracking: Use AsNoTracking() for read-only queries to improve performance.
var products = context.Products.AsNoTracking().ToList();

✔️How do you optimize large queries or batch operations?

  • Use pagination with Skip() / Take()
  • Use bulk operations via third-party libraries like EFCore.BulkExtensions
  • Project into DTOs to fetch only needed columns
var ordersDto = context.Orders
.Select(o => new { o.Id, o.Total, o.OrderDate })
.ToList();
  • context.Orders – starts with the Orders table.
  • .Select(...) – projects each order into a DTO (Data Transfer Object) that only includes the columns we care about: Id, Total, and OrderDate.
  • This reduces the amount of data pulled from the database.
  • We aren’t loading all columns like CustomerName, Address, ShippingDetails, etc., which may be large.
  • .ToList() – executes the query and brings the results into memory as a list.

Senior-Level Considerations

✅Query performance & monitoring

  • Use SQL Profiler, EF logging, or Application Insights to detect slow queries.
  • Be mindful of client evaluation — EF Core sometimes pulls data into memory unexpectedly.

✅Database design awareness

  • Indexes, relationships, and constraints impact EF queries.
  • Know when EF-generated SQL may be inefficient and optimize with raw SQL if necessary.

✅Balancing abstraction vs performance

  • Full EF tracking and lazy loading can simplify development but may hurt scalability.
  • Senior devs know when to use AsNoTracking, DTO projections, or direct SQL.

✅Concurrency and transactions

  • Use DbContext per request or per scope to avoid conflicts.
  • Handle concurrency with row versioning or optimistic locking.

A senior candidate demonstrates pragmatic EF use,
Not just knowledge of LINQ, but 
how it translates to efficient SQL and scalable apps.

6️⃣Caching Strategies & Invalidation

Caching is one of the fastest ways to improve performance, but it comes with challenges: stale datamemory pressure, and complexity. Knowing what, where, and when to cache is key in interviews.

Mid-Level Questions & Answers

✔️What types of caching exist in .NET?

  • MemoryCache: In-memory caching for single-server apps.
  • Distributed Cache: Shared cache for multi-server apps (Redis, SQL Server).
  • Response Caching: Cache HTTP responses in Web APIs.
// 1. MemoryCache for single-server in-memory caching
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCache _distributedCache;
private readonly DbContext _dbContext;

public ProductService(IMemoryCache memoryCache, IDistributedCache distributedCache, DbContext dbContext)
{
_memoryCache = memoryCache;
_distributedCache = distributedCache;
_dbContext = dbContext;
}

// MemoryCache usage
public Product GetProductFromMemory(int id)
{
return _memoryCache.GetOrCreate($"product_{id}", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return _dbContext.Products.Find(id);
});
}

// Distributed Cache usage (e.g., Redis)
public async Task<Product?> GetProductFromDistributedAsync(int id)
{
var cached = await _distributedCache.GetStringAsync($"product_{id}");
if (!string.IsNullOrEmpty(cached))
{
return JsonSerializer.Deserialize<Product>(cached);
}

var product = await _dbContext.Products.FindAsync(id);
if (product != null)
{
var serialized = JsonSerializer.Serialize(product);
await _distributedCache.SetStringAsync(
$"product_{id}",
serialized,
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }
);
}
return product;
}

// 3. Response Caching in Web API
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _service;
public ProductsController(ProductService service) => _service = service;

[HttpGet("{id}")]
[ResponseCache(Duration = 60)] // cache response for 60 seconds
public IActionResult GetProduct(int id)
{
var product = _service.GetProductFromMemory(id); // could also use distributed cache
if (product == null) return NotFound();
return Ok(product);
}
}
  • MemoryCache → Use for quick, per-server caching.
  • Distributed Cache → Use when multiple servers share data (web farm).
  • Response Caching → Use to cache API responses for repeated requests.

✔️What is cache invalidation?

Removing or updating cached data when underlying data changes.

Strategies:

  • Time-based (TTL): Automatically expire cache after a set time.
  • Event-based: Clear cache when data changes.
  • Manual: Explicitly remove or refresh entries when needed.

✔️When should you cache?

  • Frequently read, rarely updated data.
  • Expensive computations or database queries.
  • Data shared across multiple users or requests.

Senior-Level Considerations

✅Choosing the right cache type

  • MemoryCache: Fast but limited to single server; good for ephemeral data.
  • Distributed Cache (Redis): Necessary for horizontally scaled apps; supports persistence and expiration policies.

✅Cache granularity

  • Cache entire responses for simple APIs or specific objects for complex systems.
  • Avoid caching overly large objects; consider serialization cost and memory footprint.

✅Avoiding stale data & race conditions

  • Use locks or double-checked patterns to prevent multiple requests from refreshing the same cache simultaneously.

Example: Using locks

  • Ensures that only one thread loads the data from the DB and updates the cache at a time.
  • Prevents: multiple threads hitting the database simultaneously.
private static readonly object _cacheLock = new();
private readonly IMemoryCache _cache;
private readonly DbContext _dbContext;

public Product GetProduct(int id)
{
if (!_cache.TryGetValue($"product_{id}", out Product product))
{
lock (_cacheLock)
{
// double-check inside lock
if (!_cache.TryGetValue($"product_{id}", out product))
{
product = _dbContext.Products.Find(id);
_cache.Set($"product_{id}", product, TimeSpan.FromMinutes(5));
}
}
}
return product;
}
  • Consider cache-aside pattern: check cache first, load from DB if missing, then update cache.

Example: Cache-aside Pattern

  • First check the cache.
  • If missing, load from database, then update the cache.
public async Task<Product?> GetProductAsync(int id)
{
var cached = await _distributedCache.GetStringAsync($"product_{id}");
if (!string.IsNullOrEmpty(cached))
{
return JsonSerializer.Deserialize<Product>(cached);
}

// Cache miss → load from DB
var product = await _dbContext.Products.FindAsync(id);
if (product != null)
{
await _distributedCache.SetStringAsync(
$"product_{id}",
JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }
);
}
return product;
}

Benefits: Simple, avoids stale cache if used correctly, widely used with distributed caches like Redis.

✅Balancing performance vs complexity

  • Caching improves speed but adds complexity.
  • Senior devs evaluate: “Is this cache worth the extra code and potential for stale data?”

In interviews, demonstrate understanding of why caching matters, the types, and trade-offs, not just the API.

Bonus points for knowing Redis Pub/Sub for cache invalidation across servers.

7️⃣State Management (Blazor-Focused)

State management is one of the trickiest aspects of Blazor development. Unlike traditional MVC apps, Blazor apps, especially Server-Side, maintain long-lived connections, making scoped, singleton, and transient lifetimes critical.

Mid-Level Questions & Answers

✔️What are the main types of state in Blazor?

  • UI state: Component-specific variables (e.g., form inputs).
  • Application state: Shared across components (e.g., logged-in user info).
  • Session/Server state: Scoped per user connection (Blazor Server) or per browser session (WebAssembly).

✔️How do you share state across components?

  • Scoped services for per-user state:

For example, this code registers a scoped service UserState so that each user/request gets its own instance to store per-user state like UserName.

public class UserState
{
public string? UserName { get; set; }
}

builder.Services.AddScoped<UserState>(); // Program.cs
  • Inject into components:

For example, this code injects the UserState service into a Blazor component, allowing the component to access and display per-user state, such as UserName.

@inject UserState UserState

<p>Hello, @UserState.UserName</p>

✔️When to use Singleton vs Scoped?

  • Singleton: App-wide data (e.g., configuration, caching services).
  • Scoped: User/session-specific data.
  • Transient: Short-lived objects, usually for stateless services.

✔️How do you persist state across browser refreshes (WebAssembly)?

LocalStorage / SessionStorage

  • What it does: Stores simple key/value pairs in the browser. LocalStorage persists across sessions; SessionStorage clears when the tab closes.
  • Benefits: Easy to use, widely supported, no server required.
  • Trade-offs: Only stores strings, limited size (~5–10MB), data is visible to users (not secure for sensitive info).

ProtectedBrowserStorage (Blazor)

  • What it does: Wraps LocalStorage/SessionStorage with encryption to protect sensitive data.
  • Benefits: Keeps data secure in the browser, integrates smoothly with Blazor.
  • Trade-offs: Slightly more overhead, still client-side (not a substitute for server-side security).

Serialization for Complex Objects

  • What it does: Converts objects to JSON (or another format) before storing in the browser.
  • Benefits: Lets you store structured or nested data, not just strings.
  • Trade-offs: Serialization/deserialization adds complexity and processing time; large objects can impact performance.
await localStorage.SetItemAsync("username", "Robyn");
var username = await localStorage.GetItemAsync<string>("username");

Senior-Level Considerations

✅Blazor Server vs WebAssembly differences:

  • Server: Scoped services live per user connection; concurrent users require careful management.
  • WebAssembly: Singleton services are shared across the user’s session; state persists until the page is refreshed or closed.

✅Avoiding state-related bugs:

  • Don’t store large mutable objects in singleton services unless thread-safe.
  • Be cautious with event callbacks that modify shared state.

✅Patterns for state management:

  • Flux/Redux-like approach: For complex apps with many interactions.
  • Observer pattern: Components react to state changes via events or NotifyPropertyChanged.

✅Performance considerations:

  • Avoid unnecessary component re-rendering by using ShouldRender() or StateHasChanged() carefully.
  • Combine caching and scoped services to minimize expensive data fetches per render.

Senior devs know when to lift state to a service vs keep it in a component, and how to balance performance, concurrency, and maintainability in Blazor apps.

8️⃣Performance & Scalability Under Load

High-performance applications aren’t just fast — they’re resilient, maintainable, and scalable. In interviews, candidates are expected to show not just knowledge of code, but judgment under real-world constraints.

Mid-Level Questions & Answers

✔️How do you improve API performance in .NET?

  • Asynchronous programming: Use async/await to avoid blocking threads.
  • Pagination & filtering: Avoid returning large datasets.
  • Efficient queries: Use projections, AsNoTracking(), and proper indexing.
var topProducts = await context.Products
.AsNoTracking()
.OrderByDescending(p => p.Sales)
.Take(50)
.ToListAsync();

✔️How do you reduce load on the database?

  • Caching frequently accessed data (MemoryCache, Redis).
  • Denormalizing read-heavy data for fast queries.
  • Batching operations instead of multiple round trips.

✔️What are common threading pitfalls in .NET?

  • Blocking calls in async methods (.Result or .Wait()), which can lead to thread starvation.
  • Shared mutable state in singleton services causing race conditions.

When you store data that can change (“mutable”) inside a singleton service (one instance shared across the entire application), multiple users or requests can access and modify it at the same time, leading to race conditions: unexpected or incorrect behavior because the service’s state is being changed concurrently.

In this example, Users may see wrong count values because Count is shared and updated concurrently.

public class CounterService
{
public int Count { get; set; } = 0; // mutable state
}

// Registered as singleton
builder.Services.AddSingleton<CounterService>();

// Multiple requests incrementing Count at the same time
counterService.Count++; // could overwrite each other

✔️How do you profile and monitor performance?

  • Use Application Insights, MiniProfiler, or dotnet-trace.
  • Check SQL execution, HTTP latency, and memory usage.

Senior-Level Considerations

✅Scalability strategies

  • Horizontal scaling: Multiple instances behind a load balancer.
  • Vertical scaling: More resources per instance; limited by hardware.
  • Blazor Server considerations: Use SignalR efficiently; avoid heavy per-connection memory usage.

✅Async patterns & throughput

  • Use ValueTask for high-frequency methods.
  • Avoid blocking threads in ASP.NET Core middleware or long-running tasks.

✅Concurrency & thread safety

  • Carefully handle singleton services shared across users.
  • Use locks, ConcurrentDictionary, or immutable objects for shared state.

✅Profiling and bottleneck detection

  • EF Core: Check for N+1 queries or inefficient joins.
  • HTTP APIs: Monitor response times, serialization overhead, and logging costs.
  • Memory & GC: Monitor allocations; avoid large object heap fragmentation.

✅Design trade-offs

  • Optimizing for latency vs throughput: sometimes batching requests reduces throughput but improves average response time.
  • Cache vs consistency: More caching improves performance but risks stale data. Senior devs know the balance.

In interviews, focus on practical patterns: async programming, caching, batching, and efficient queries — combined with awareness of trade-offs for scaling and resource usage.

9️⃣Logging, Observability & Diagnostics

Good logging isn’t just about writing messages, it’s about understanding system behavior, diagnosing issues, and monitoring performance. Interviewers want to see both knowledge of tools and judgment in applying them.

Mid-Level Questions & Answers

✔️What logging options exist in .NET?

✴️ILogger (built-in .NET logging): The built-in logging abstraction/interface provided by .NET (Microsoft.Extensions.Logging).

Benefits:

  • Works out of the box with ASP.NET Core.
  • Flexible: can write to console, debug, files, or plug in other providers.
  • Supports structured logging.

Trade-offs:

  • Basic functionality alone; for advanced features (like rolling files, structured sinks), you often plug in a framework.

Best used when:

  • You want lightweight logging, consistent across your app, or as a foundation to integrate other frameworks.

✴️️Serilog / NLog / log4net (structured logging frameworks)
Full-featured logging frameworks that provide structured logging, sinks, formatting, and filtering.

Benefits:

  • Can log to multiple destinations (“sinks”) like files, databases, console, Seq, or ELK stack.
  • Rich formatting and filtering options.
  • Serilog supports strongly typed structured logging, great for analytics.

Trade-offs:

  • Extra dependency/configuration.
  • More setup than ILogger alone.

Best used when:

  • You need advanced logging features, multiple output destinations, or analytics-ready structured logs.

✴️Application Insights (cloud-based telemetry)
Microsoft Azure service for telemetry, performance tracking, and exception logging.

Benefits:

  • Captures real-time telemetry, performance metrics, exceptions, and traces.
  • Excellent for monitoring production applications in Azure.
  • Integrates with ILogger, Serilog, etc.

Trade-offs:

  • Requires Azure subscription.
  • Adds network/latency overhead for sending logs to the cloud.

Best used when:

  • You need application monitoring, diagnostics, and performance insights in production, especially for distributed/cloud apps.
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) => _logger = logger;

public void ProcessOrder(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
// processing logic
}
}

✔️What is structured logging and why use it?

  • Structured logs store key-value pairs instead of plain text.
  • Makes filtering, querying, and analyzing logs much easier.

✔️How do you capture exceptions effectively?

  • Use try/catch with logging and context.
  • Avoid swallowing exceptions silently.
  • Log critical information: request IDs, user context, stack trace.
try
{
service.ProcessOrder(orderId);
}
catch(Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}

✔️What is observability?

  • Observability = metrics + logs + traces
  • Allows you to understand system behavior, spot performance issues, and debug production problems.

Senior-Level Considerations

✅Correlation & distributed tracing

  • Use correlation IDs to track requests across services.
  • Leverage OpenTelemetry or Application Insights for end-to-end tracing.

✅Log level discipline

  • Avoid logging everything at Information level; use Debug for verbose logs, Warning for unexpected states, Error for failures, Critical for system-breaking issues.

✅Performance impact

  • Structured logging can be more expensive. Use async logging sinks to avoid blocking threads.
  • Avoid logging sensitive data or PII.

✅Diagnostics & monitoring strategy

  • Track key metrics: response time, request rate, exception rates, database latency.
  • Set up alerts for anomalies or thresholds exceeded.

✅Design trade-offs

  • Senior developers balance visibility vs noise: too much logging adds overhead, too little makes debugging impossible.
  • Use feature toggles or environment-based logging levels to adjust verbosity dynamically.

Senior candidates articulate not just how to log, but how to design observability into the application, making production support predictable and manageable.

10. Security & Cross-Cutting Concerns

In .NET applicationssecurity and cross-cutting concerns go beyond just writing controllers. They affect every layer of the application. Senior developers must anticipate risks, enforce policies, and implement reusable solutions.

Mid-Level Questions & Answers

✔️Authentication vs Authorization

  • Authentication: Confirms the user’s identity (e.g., ASP.NET Core Identity, JWT).
  • Authorization: Determines what an authenticated user can do (policies, roles, claims).
[Authorize(Roles = "Admin")]
public IActionResult DeleteUser(int id) { ... }

✔️What are common cross-cutting concerns?

Cross-cutting concerns are aspects of an application that affect multiple layers or modules rather than just one. They “cut across” the core business logic.

  • Logging — Track what happens in your app.
  • Caching — Store frequently used data for performance.
  • Validation — Ensure inputs meet rules across endpoints.
  • Exception Handling — Centralized error management.
  • Security — Authentication, authorization, encryption.
  • Telemetry/Monitoring — Performance tracking, metrics.
  • Transactions — Ensuring multiple operations succeed or fail together.

✔️How do you handle input validation?

  • Use DataAnnotations or FluentValidation for model validation.
  • Always validate at the API boundary, never rely on client validation alone.

You would use FluentValidation over DataAnnotations when you need more flexible, reusable, and complex validation rules that go beyond simple attribute-based checks.

Example: Data Annotations

[Required]
[EmailAddress]
public string Email { get; set; }

Example: Fluent Validation

// 1. Install FluentValidation via NuGet
// dotnet add package FluentValidation

// 2. Create your model
public class UserRegistration
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public int Age { get; set; }
}

// 3. Create a validator
using FluentValidation;

public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
public UserRegistrationValidator()
{
RuleFor(u => u.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Must be a valid email");

RuleFor(u => u.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(6).WithMessage("Password must be at least 6 characters");

RuleFor(u => u.Age)
.GreaterThanOrEqualTo(18).WithMessage("Must be at least 18 years old");
}
}

// 4. Validate in your service or controller
public class UserService
{
private readonly IValidator<UserRegistration> _validator;

public UserService(IValidator<UserRegistration> validator)
{
_validator = validator;
}

public void Register(UserRegistration user)
{
var result = _validator.Validate(user);
if (!result.IsValid)
{
// handle validation errors
throw new ValidationException(result.Errors);
}

// continue with registration
}
}

✔️How do you prevent common security vulnerabilities?

  • SQL Injection: Use parameterized queries or EF Core.
  • XSS: Razor automatically HTML-encodes output; sanitize user input.
  • CSRF: Use anti-forgery tokens in forms (ASP.NET Core handles this by default).
  • Sensitive data: Don’t store secrets in code; use Azure Key Vault or environment variables.

Senior-Level Considerations

✅Designing security as cross-cutting

  • Use middleware or filters for authentication, authorization, and logging.
  • Avoid sprinkling security checks throughout business logic.

✅Claims-based and policy-based authorization

  • Policy-based approaches are more flexible than role-based.
  • Consider multi-tenant scenarios, feature flags, or hierarchical permissions.

✅Exception handling & resilience

  • Global exception handling via middleware keeps APIs consistent and reduces leaks of sensitive information.
  • Combine with retry policies (e.g., Polly) for transient failures.

✅Performance vs security trade-offs

  • Hashing passwords with a strong algorithm (e.g., PBKDF2, bcrypt) may be slower, but necessary.
  • Senior devs balance user experience, system performance, and security rigor.

✅Auditing & compliance

  • Log critical security events (login attempts, privilege escalations).
  • Ensure logging complies with GDPR, HIPAA, or other regulations.

Senior candidates articulate how cross-cutting concerns are centralizedautomated, and enforced consistently, not just sprinkled throughout the codebase.

Mastering .NET isn’t just about knowing syntax or patterns — it’s about applying principles with judgment.

From SOLID principles to Dependency InjectionMediatR/CQRS, and Vertical Slice Architecture, a solid foundation allows you to write clean, maintainable code. But senior developers stand out by balancing theory with pragmatism â€” understanding when to apply patterns, optimize performance, or enforce security without over-engineering.

In advanced topics like Entity Framework optimization, caching strategies, and Blazor state management, interviewers look for your ability to predict pitfalls, scale systems, and reason about trade-offs. Likewise, logging, observability, and cross-cutting concerns showcase your readiness to maintain resilient, production-ready applications.

The most compelling mid-to-senior candidates don’t just answer questions — they demonstrate judgment, showing how technical decisions impact maintainability, performance, and security across an application.

In short, understanding the “why” behind the “how” separates competent developers from exceptional ones. By combining solid fundamentals with real-world judgment, you’re not just prepared for interviews — you’re prepared to build systems that scale, perform, and endure.

Happy coding!