C Sharp Dotnet interview programming questions for experienced programmers
C# · .NET · Interview preparation
59 C# and .NET Interview Programs, Explained
These examples cover algorithms, object-oriented design, asynchronous programming, data access, APIs, and testing. Each section explains the intended behavior and its limits. Examples target modern C# and .NET 10 unless a framework or package is named.
Read each example independently. Put standalone methods inside a suitable class, and put usage statements inside a method (or before type declarations in a top-level program). Common namespaces include System, System.Collections.Generic, System.Collections.Concurrent, System.Linq, System.Text, System.Net.Http, System.Threading, and System.Threading.Tasks. EF Core, ASP.NET Core, MSTest, Moq, and API versioning examples require their packages and application types. Snippets that illustrate an application boundary are identified in the explanation; this article is not one combined executable application.
1. Reverse a String
Reverse the order of Unicode text elements so a surrogate pair or combining sequence stays together. This is text reversal, not a bidirectional-layout algorithm. Time and additional storage are O(n).
public static string ReverseString(string input)
{
ArgumentNullException.ThrowIfNull(input);
var elements = new List<string>();
var iterator = System.Globalization.StringInfo.GetTextElementEnumerator(input);
while (iterator.MoveNext()) elements.Add(iterator.GetTextElement());
elements.Reverse();
return string.Concat(elements);
}
Reference: Unicode text elements.
2. Check if String is Palindrome
This contract ignores non-letter/digit Unicode scalars and compares invariant uppercase forms. Empty input is a palindrome; null is rejected. It does not perform linguistic equivalence or full Unicode case folding. Normalize first if canonically equivalent forms must compare alike.
public static bool IsPalindrome(string input)
{
ArgumentNullException.ThrowIfNull(input);
var letters = input.EnumerateRunes()
.Where(System.Text.Rune.IsLetterOrDigit)
.Select(System.Text.Rune.ToUpperInvariant).ToArray();
for (int left = 0, right = letters.Length - 1; left < right; left++, right--)
if (letters[left] != letters[right]) return false;
return true;
}
3. Find Missing Number in Array
Exactly one value is missing from 1 through n. Validation costs O(n) additional memory; if the distinct/range preconditions are already guaranteed, a sum or XOR solution can use O(1) additional space.
public static int FindMissingNumber(int[] values, int n)
{
ArgumentNullException.ThrowIfNull(values);
if (n < 1 || values.Length != n - 1)
throw new ArgumentException("Expected n - 1 distinct values from 1 through n.");
var seen = new HashSet<int>();
long actual = 0;
foreach (int value in values)
{
if (value < 1 || value > n || !seen.Add(value))
throw new ArgumentException("Values must be distinct and in range.");
actual += value;
}
return (int)((long)n * (n + 1L) / 2 - actual);
}
4. Fibonacci Series
Produce n terms starting at 0. BigInteger avoids fixed-width overflow, but term sizes and arithmetic costs grow with n. Iterator argument validation runs when enumeration begins.
public static IEnumerable<System.Numerics.BigInteger> Fibonacci(int n)
{
if (n < 0) throw new ArgumentOutOfRangeException(nameof(n));
System.Numerics.BigInteger a = 0, b = 1;
for (int i = 0; i < n; i++)
{
yield return a;
(a, b) = (b, a + b);
}
}
5. Prime Number Check
Trial division only needs odd divisors up to the square root. Runtime is O(sqrt(n)); extra space is O(1). Negative values, 0, and 1 are not prime.
public static bool IsPrime(int number)
{
if (number < 2) return false;
if (number == 2) return true;
if (number % 2 == 0) return false;
for (int i = 3; i <= number / i; i += 2)
{
if (number % i == 0) return false;
}
return true;
}
6. Factorial Calculation
0! is 1. A signed 64-bit integer can store factorials only through 20!, so this implementation rejects larger inputs. Use BigInteger when a larger exact result is required.
public static long Factorial(int n)
{
if (n < 0 || n > 20) throw new ArgumentOutOfRangeException(nameof(n), "Use 0 through 20 for Int64 factorial.");
if (n <= 1) return 1;
long result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
}
7. Bubble Sort Implementation
Bubble sort mutates the input in place, is stable with the strict greater-than comparison, and takes O(n²) time in this unoptimized implementation. It assumes a non-null array. An early-exit flag can improve already-sorted cases.
public static void BubbleSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
8. Binary Search
Binary search requires a non-null array sorted in ascending order. It returns an arbitrary matching index when duplicates exist, or -1 when absent. Runtime is O(log n).
public static int BinarySearch(int[] arr, int target)
{
int left = 0, right = arr.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
9. Find Duplicates in Array
The output contains each duplicate value once, in the order its second occurrence is encountered. Expected runtime is O(n), with O(n) additional storage.
public static List<int> FindDuplicates(int[] values)
{
ArgumentNullException.ThrowIfNull(values);
var seen = new HashSet<int>();
var emitted = new HashSet<int>();
var result = new List<int>();
foreach (int value in values)
if (!seen.Add(value) && emitted.Add(value)) result.Add(value);
return result;
}
10. Count Character Occurrences
This counts UTF-16 char values, not complete Unicode characters or grapheme clusters, and requires a non-null input. Use EnumerateRunes or StringInfo for a different definition of character.
public static Dictionary<char, int> CountCharacters(string input)
{
return input.GroupBy(c => c)
.ToDictionary(g => g.Key, g => g.Count());
}
11. Singleton Pattern Implementation
Lazy<T> uses thread-safe initialization by default. It makes creation safe; mutable state inside the singleton still needs synchronization. Constructor injection often makes service lifetime and testing easier.
public sealed class Singleton
{
private static readonly Lazy<Singleton> _instance =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => _instance.Value;
private Singleton() { }
}
12. Factory Pattern
This is a simple factory, not the full Factory Method inheritance pattern. Centralized selection works for a small closed set of types; a registry can support extensions without editing the switch.
public interface IVehicle
{
void Drive();
}
public class Car : IVehicle
{
public void Drive() => Console.WriteLine("Driving car");
}
public class Bike : IVehicle
{
public void Drive() => Console.WriteLine("Riding bike");
}
public class VehicleFactory
{
public static IVehicle CreateVehicle(string type)
{
return (type ?? throw new ArgumentNullException(nameof(type))).ToLowerInvariant() switch
{
"car" => new Car(),
"bike" => new Bike(),
_ => throw new ArgumentException("Invalid vehicle type")
};
}
}
13. Observer Pattern
The subject notifies observers synchronously. This minimal example assumes no concurrent subscriptions and no subscription changes during notification. A snapshot avoids collection modification errors; exception and unsubscription policies must also be defined.
public interface IObserver
{
void Update(string message);
}
public class Subject
{
private readonly List<IObserver> _observers = new();
public void Attach(IObserver observer) => _observers.Add(observer);
public void Detach(IObserver observer) => _observers.Remove(observer);
public void Notify(string message)
{
foreach (var observer in _observers)
{
observer.Update(message);
}
}
}
14. Strategy Pattern
A strategy lets the caller select payment behavior through an interface. These implementations only print a demonstration message; they do not connect to a payment provider.
public interface IPaymentStrategy { void Pay(decimal amount); }
public sealed class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"Demo credit card payment: {amount:C}");
}
public sealed class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"Demo PayPal payment: {amount:C}");
}
public sealed class ShoppingCart
{
private IPaymentStrategy _strategy;
public ShoppingCart(IPaymentStrategy strategy) =>
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
public void SetPaymentStrategy(IPaymentStrategy strategy) =>
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
public void Checkout(decimal amount)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount));
_strategy.Pay(amount);
}
}
15. Abstract Class vs Interface
An abstract class can share instance state and implementation; a class has only one direct class base but can implement multiple interfaces. Modern C# interfaces can contain default implementations and static members, so “interfaces never contain code” is inaccurate.
// Abstract Class
public abstract class Animal
{
public string Name { get; set; }
public abstract void MakeSound();
public virtual void Sleep() => Console.WriteLine("Sleeping");
}
// Interface
public interface IMovable
{
void Move();
int Speed { get; }
}
public class Dog : Animal, IMovable
{
public int Speed => 20;
public override void MakeSound() => Console.WriteLine("Woof!");
public void Move() => Console.WriteLine($"Moving at {Speed} km/h");
}
16. Custom Collection Implementation
Delegating to List<T> provides ordinary generic and nongeneric enumeration. The wrapper is not thread-safe, and modifying its list during enumeration invalidates the enumerator.
public class CustomCollection<T> : IEnumerable<T>
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public void Remove(T item) => _items.Remove(item);
public int Count => _items.Count;
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
17. LINQ Group By with Multiple Keys
An anonymous composite key groups by both Age and City. LINQ-to-Objects GroupBy is deferred until enumeration and buffers groups; this method only constructs the query, so enumerate it or return it to expose results.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string City { get; set; }
}
public static void GroupByMultipleKeys(List<Person> people)
{
var grouped = people.GroupBy(p => new { p.Age, p.City })
.Select(g => new
{
Age = g.Key.Age,
City = g.Key.City,
Count = g.Count(),
Names = g.Select(p => p.Name).ToList()
});
}
18. Custom LINQ Extension Method
WhereIf composes a filter only when the condition is true. State non-null source and predicate preconditions; LINQ-to-Objects filtering is deferred. IQueryable requires expression trees if filtering should be translated by a provider.
public static class EnumerableExtensions
{
public static IEnumerable<T> WhereIf<T>(
this IEnumerable<T> source,
bool condition,
Func<T, bool> predicate)
{
return condition ? source.Where(predicate) : source;
}
}
19. Dictionary with Custom Comparer
Equal keys must produce equal hash codes. Delegate both operations to the same StringComparer. OrdinalIgnoreCase is appropriate for many nonlinguistic identifiers; choose a documented comparison policy for user language.
public sealed class CaseInsensitiveComparer : IEqualityComparer<string>
{
public bool Equals(string? x, string? y) =>
StringComparer.OrdinalIgnoreCase.Equals(x, y);
public int GetHashCode(string value) =>
StringComparer.OrdinalIgnoreCase.GetHashCode(value);
}
// Usually prefer the built-in comparer directly:
// var values = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
Reference: StringComparer.
20. Concurrent Collections
ConcurrentDictionary synchronizes dictionary operations. GetOrAdd may invoke its factory more than once during a race, and stored mutable objects do not become thread-safe. The factory must tolerate redundant execution.
public class ThreadSafeCache<TKey, TValue> where TKey : notnull
{
private readonly ConcurrentDictionary<TKey, TValue> _cache = new();
public TValue GetOrAdd(TKey key, Func<TKey, TValue> factory)
{
return _cache.GetOrAdd(key, factory);
}
public bool TryRemove(TKey key, out TValue value)
{
return _cache.TryRemove(key, out value);
}
}
Reference: ConcurrentDictionary.GetOrAdd.
21. Async/Await Best Practices
The injected client can be registered as a typed client with AddHttpClient. Task.WhenAll preserves input order in its results but starts an operation for every URL; use bounded concurrency for large input sets. Do not perform concurrent EF operations on one DbContext.
public sealed class AsyncService
{
private readonly HttpClient _client;
public AsyncService(HttpClient client) => _client = client;
public Task<string> GetDataAsync(string url, CancellationToken ct = default) =>
_client.GetStringAsync(url, ct);
public Task<string[]> ProcessMultipleAsync(IEnumerable<string> urls,
CancellationToken ct = default) =>
Task.WhenAll(urls.Select(url => GetDataAsync(url, ct)));
}
// In an ASP.NET Core application:
// builder.Services.AddHttpClient<AsyncService>();
Reference: HttpClient lifetime guidelines, DbContext lifetime and configuration.
22. CancellationToken Usage
Cancellation is cooperative. Let OperationCanceledException propagate unless this layer deliberately converts it into another documented outcome; otherwise the returned task can appear successful.
public async Task ProcessWithCancellationAsync(CancellationToken cancellationToken)
{
await Task.Delay(5000, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
// Continue the operation, passing the token to cancellable calls.
}
23. Async Stream Processing
Async streams produce values incrementally and can suspend between elements. EnumeratorCancellation allows enumeration cancellation to reach the iterator token. The consumer can use WithCancellation(token).
public async IAsyncEnumerable<int> GenerateNumbersAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100, ct);
yield return i;
}
}
public async Task ProcessAsyncStream()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
}
24. TaskCompletionSource Example
TaskCompletionSource bridges an external completion signal to a task. This is a one-shot event: after completion, subsequent signal attempts return false and future waiters immediately observe the same completion.
public class AsyncEventSource
{
private readonly TaskCompletionSource<bool> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task<bool> WaitForEventAsync() => _tcs.Task;
public void SignalEvent()
{
_tcs.TrySetResult(true);
}
public void SignalError(Exception ex)
{
_tcs.TrySetException(ex);
}
}
Reference: Task creation options.
25. Parallel Processing with PLINQ
PLINQ can help CPU-heavy independent work, but overhead may dominate this small calculation. Output ordering is not preserved unless requested with AsOrdered. Checked arithmetic exposes overflow.
public static List<int> ProcessInParallel(List<int> numbers)
{
return numbers.AsParallel()
.Where(n => n % 2 == 0)
.Select(n => checked(n * n))
.ToList();
}
26. Repository Pattern
Requires Microsoft.EntityFrameworkCore and its namespace. The generic repository assumes a single Int32 primary key. Add/Update/Delete alter tracked state; question 27 commits changes. Updating a disconnected entity broadly can overwrite fields, so use an allow-listed mapping for external input.
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);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public sealed class EfRepository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
public EfRepository(DbContext context) => _context = context;
public async Task<T?> GetByIdAsync(int id, CancellationToken ct = default) =>
await _context.Set<T>().FindAsync(new object[] { id }, ct);
public Task<List<T>> GetAllAsync(CancellationToken ct = default) =>
_context.Set<T>().ToListAsync(ct);
public void Add(T entity) => _context.Set<T>().Add(entity);
public void Update(T entity) => _context.Set<T>().Update(entity);
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
{
var entity = await GetByIdAsync(id, ct);
if (entity is null) return false;
_context.Set<T>().Remove(entity);
return true;
}
}
Reference: DbContext lifetime and configuration.
27. Unit of Work Pattern
EF Core DbContext already provides unit-of-work behavior. This optional wrapper only exposes commit. Repository and wrapper must receive the same scoped context; the DI scope owns disposal. For separate SaveChanges calls, atomicity requires an explicit transaction where supported.
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
public sealed class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
public UnitOfWork(DbContext context) => _context = context;
public Task<int> SaveChangesAsync(CancellationToken ct = default) =>
_context.SaveChangesAsync(ct);
}
Reference: DbContext lifetime and configuration, Dependency injection guidelines.
28. Command Pattern
This example uses User with an integer Id, the repository from question 26, and the unit of work from question 27. Undo here means deleting the inserted user; it is not universally reversible after later changes, relationships, or external side effects.
public interface IAsyncCommand
{
Task ExecuteAsync(CancellationToken ct = default);
Task UndoAsync(CancellationToken ct = default);
}
public sealed class AddUserCommand : IAsyncCommand
{
private readonly User _user;
private readonly IRepository<User> _repository;
private readonly IUnitOfWork _unitOfWork;
public AddUserCommand(User user, IRepository<User> repository, IUnitOfWork unitOfWork)
=> (_user, _repository, _unitOfWork) = (user, repository, unitOfWork);
public async Task ExecuteAsync(CancellationToken ct = default)
{
_repository.Add(_user);
await _unitOfWork.SaveChangesAsync(ct);
}
public async Task UndoAsync(CancellationToken ct = default)
{
await _repository.DeleteAsync(_user.Id, ct);
await _unitOfWork.SaveChangesAsync(ct);
}
}
29. Mediator Pattern
This demonstrates the request/handler contract with the repository from question 26 and an application User type. A mediator dispatcher and DI registrations are additional infrastructure. These interfaces are illustrative, not a claim about a third-party package API.
public interface IRequest<TResponse> { }
public interface IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken ct = default);
}
public sealed record GetUserQuery(int UserId) : IRequest<User?>;
public sealed class GetUserHandler : IRequestHandler<GetUserQuery, User?>
{
private readonly IRepository<User> _repository;
public GetUserHandler(IRepository<User> repository) => _repository = repository;
public Task<User?> Handle(GetUserQuery request, CancellationToken ct = default) =>
_repository.GetByIdAsync(request.UserId, ct);
}
30. Decorator Pattern
A decorator wraps the same interface and adds behavior through composition. For monetary calculations, prefer decimal and decimal literals; double here only demonstrates the wrapping pattern.
public interface ICoffee
{
string GetDescription();
double GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Simple Coffee";
public double GetCost() => 1.0;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual double GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override double GetCost() => _coffee.GetCost() + 0.5;
}
31. Single Responsibility Principle
SRP concerns reasons to change. The service coordinates persistence and email through separate contracts; define IUserRepository.AddAsync(User) and IEmailService.SendWelcomeEmailAsync(string) to return Task. Email can fail after persistence; an outbox can provide reliable delivery when needed.
// Bad: Multiple responsibilities
public class UserManager
{
public void CreateUser(User user) { }
public void SendEmail(string to, string subject) { }
public void SaveToDatabase(User user) { }
}
// Good: Single responsibility
public class UserService
{
private readonly IUserRepository _repository;
private readonly IEmailService _emailService;
public UserService(IUserRepository repository, IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public async Task CreateUserAsync(User user)
{
await _repository.AddAsync(user);
await _emailService.SendWelcomeEmailAsync(user.Email);
}
}
32. Open/Closed Principle
The example extends payment behavior with derived classes without changing the abstraction. The comments are implementation placeholders, not working payment integrations. Apply this pattern where variation is expected.
public abstract class PaymentProcessor
{
public abstract void ProcessPayment(decimal amount);
}
public class CreditCardProcessor : PaymentProcessor
{
public override void ProcessPayment(decimal amount)
{
// Process credit card payment
}
}
public class PayPalProcessor : PaymentProcessor
{
public override void ProcessPayment(decimal amount)
{
// Process PayPal payment
}
}
33. Liskov Substitution Principle
A subtype should preserve the observable behavioral contract of its base. A penguin can satisfy Bird.Eat without promising flight. Flight belongs on the separate IFlyable capability.
public abstract class Bird
{
public abstract void Eat();
}
public interface IFlyable { void Fly(); }
public sealed class Sparrow : Bird, IFlyable
{
public override void Eat() => Console.WriteLine("Eating seeds");
public void Fly() => Console.WriteLine("Flying");
}
public sealed class Penguin : Bird
{
public override void Eat() => Console.WriteLine("Eating fish");
}
34. Interface Segregation Principle
Clients should depend on the capabilities they use. These empty method bodies illustrate interface boundaries only; they do not implement work, eating, or sleeping behavior.
// Bad: Fat interface
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
// Good: Segregated interfaces
public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class Human : IWorkable, IEatable, ISleepable
{
public void Work() { }
public void Eat() { }
public void Sleep() { }
}
public class Robot : IWorkable
{
public void Work() { }
// No Eat or Sleep methods
}
35. Dependency Inversion Principle
High-level order logic depends on INotifier rather than a concrete email implementation. Supply the application Order type and a notifier implementation; these method bodies show the dependency boundary.
public interface INotifier
{
void Notify(string message);
}
public class EmailNotifier : INotifier
{
public void Notify(string message) { }
}
public class OrderService
{
private readonly INotifier _notifier;
public OrderService(INotifier notifier) => _notifier = notifier;
public void PlaceOrder(Order order)
{
// Process order
_notifier.Notify("Order placed");
}
}
36. Service Lifetime Management
Singleton means one instance per service provider; scoped means one per scope, commonly a web request; transient means one per resolution. Singleton services must be thread-safe and must not capture scoped services. Startup is an older hosting style; the same registrations can use builder.Services.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Singleton: One instance for the entire application
services.AddSingleton<IConfigurationService, ConfigurationService>();
// Scoped: One instance per HTTP request
services.AddScoped<IUserService, UserService>();
// Transient: New instance every time
services.AddTransient<IEmailService, EmailService>();
}
}
Reference: Dependency injection guidelines.
37. Custom Service Factory
Register EmailService, SmsService, and MessageServiceFactory, for example as scoped services, and resolve the factory inside that scope. The service-provider approach is a small service locator; explicit constructor dependencies or keyed services can make dependencies clearer.
public interface IMessageService
{
void SendMessage(string message);
}
public class EmailService : IMessageService
{
public void SendMessage(string message) => Console.WriteLine($"Email: {message}");
}
public class SmsService : IMessageService
{
public void SendMessage(string message) => Console.WriteLine($"SMS: {message}");
}
public class MessageServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public MessageServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMessageService Create(string type)
{
return (type ?? throw new ArgumentNullException(nameof(type))).ToLowerInvariant() switch
{
"email" => _serviceProvider.GetRequiredService<EmailService>(),
"sms" => _serviceProvider.GetRequiredService<SmsService>(),
_ => throw new ArgumentException("Invalid message type")
};
}
}
Reference: Dependency injection guidelines.
38. Conditional Registration
Choose registrations during application startup. Switching an environment variable later does not replace already registered services. The mock and real implementations must both satisfy their interfaces.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
if (Environment.GetEnvironmentVariable("USE_MOCK_SERVICES") == "true")
{
services.AddScoped<IUserService, MockUserService>();
services.AddScoped<IEmailService, MockEmailService>();
}
else
{
services.AddScoped<IUserService, UserService>();
services.AddScoped<IEmailService, EmailService>();
}
}
}
39. Service Collection Extensions
Registration extension methods organize application wiring. Supply the service implementations and configure the correct EF database provider and connection string; AddDbContext alone does not choose a provider.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IUserService, UserService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IProductService, ProductService>();
return services;
}
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}
}
40. Circular Dependency Resolution
Remove mutual service construction dependencies by moving coordination into a third service. Register each implementation and the workflow. If both writes must succeed together, add an appropriate transaction or cross-service compensation strategy.
public interface IUserCreator
{
Task<int> CreateAsync(CancellationToken ct);
}
public interface IOrderCreator
{
Task CreateForUserAsync(int userId, CancellationToken ct);
}
public sealed class RegistrationWorkflow
{
private readonly IUserCreator _users;
private readonly IOrderCreator _orders;
public RegistrationWorkflow(IUserCreator users, IOrderCreator orders)
=> (_users, _orders) = (users, orders);
public async Task RegisterAsync(CancellationToken ct = default)
{
int userId = await _users.CreateAsync(ct);
await _orders.CreateForUserAsync(userId, ct);
}
}
Reference: Dependency injection guidelines.
41. Code-First Configuration
This is an EF Core SQL Server mapping: GETDATE() and the identity syntax in question 45 are provider-specific. Apply the configuration in OnModelCreating or ApplyConfigurationsFromAssembly. Define Order and initialize Orders. Database collation determines email uniqueness semantics.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(u => u.Id);
builder.Property(u => u.Email).IsRequired().HasMaxLength(100);
builder.HasIndex(u => u.Email).IsUnique();
builder.Property(u => u.CreatedAt).HasDefaultValueSql("GETDATE()");
}
}
Reference: DbContext lifetime and configuration.
42. Complex Query with Include
Eager loading retrieves the navigation graph. Define the referenced entity properties in the model. Large collection joins may duplicate data; compare projections or split queries after measuring. The query returns null when the user does not exist.
public async Task<User?> GetUserWithOrdersAsync(int userId)
{
return await _context.Users
.Include(u => u.Orders)
.ThenInclude(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.Include(u => u.Profile)
.FirstOrDefaultAsync(u => u.Id == userId);
}
43. Raw SQL Query
The shown {0} placeholder with a separate argument is parameterized. Do not interpolate untrusted values into a raw SQL string. This example assumes SQL Server and an entity-shaped result containing required mapped columns; stored-procedure results cannot be freely composed as a SQL subquery.
public async Task<List<User>> GetUsersByCityAsync(string city)
{
return await _context.Users
.FromSqlRaw("SELECT * FROM Users WHERE City = {0}", city)
.ToListAsync();
}
public async Task<List<User>> GetUsersWithStoredProcedureAsync(int minAge)
{
return await _context.Users
.FromSqlRaw("EXEC GetUsersByAge {0}", minAge)
.ToListAsync();
}
Reference: EF Core SQL queries.
44. Change Tracking
SetValues copies mapped scalar values onto a tracked entity; it does not reconcile an entire relationship graph. Prefer an explicit allow-list for client edits, and handle concurrency tokens. Entries reports tracked entity state, not a full database audit history.
public async Task UpdateUserAsync(User user)
{
var existingUser = await _context.Users.FindAsync(user.Id);
if (existingUser != null)
{
_context.Entry(existingUser).CurrentValues.SetValues(user);
await _context.SaveChangesAsync();
}
}
public void TrackEntityChanges()
{
var entries = _context.ChangeTracker.Entries();
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
Console.WriteLine($"Added: {entry.Entity}");
break;
case EntityState.Modified:
Console.WriteLine($"Modified: {entry.Entity}");
break;
case EntityState.Deleted:
Console.WriteLine($"Deleted: {entry.Entity}");
break;
}
}
}
45. Database Migration
This SQL Server migration adds a profile table and a unique UserId index, allowing at most one profile per user. It does not require every user to have a profile. Generate and review migrations against the actual model; dropping the table loses its data.
public partial class AddUserProfile : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserProfiles",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<int>(type: "int", nullable: false),
Bio = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()")
},
constraints: table =>
{
table.PrimaryKey("PK_UserProfiles", x => x.Id);
table.ForeignKey(
name: "FK_UserProfiles_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_UserProfiles_UserId",
table: "UserProfiles",
column: "UserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "UserProfiles");
}
}
46. RESTful Controller
The controller demonstrates resource-oriented routes, 201 with a Location header, 404 for missing records, and 204 after update/delete. Supply service contracts and DTOs. Validate and bound pagination; enforce authorization. PUT should represent replacement semantics or its narrower contract should be explicit.
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService) => _userService = userService;
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetUsers([FromQuery] int page = 1, int pageSize = 10)
{
var users = await _userService.GetUsersAsync(page, pageSize);
return Ok(users);
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _userService.GetUserByIdAsync(id);
if (user == null) return NotFound();
return Ok(user);
}
[HttpPost]
public async Task<ActionResult<User>> CreateUser([FromBody] CreateUserRequest request)
{
var user = await _userService.CreateUserAsync(request);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
var success = await _userService.UpdateUserAsync(id, request);
if (!success) return NotFound();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
var success = await _userService.DeleteUserAsync(id);
if (!success) return NotFound();
return NoContent();
}
}
47. Custom Action Filter
ApiController already returns automatic validation errors. A custom filter can be useful without that behavior; register ValidationFilter as a service for ServiceFilter. Prefer configuring InvalidModelStateResponseFactory when customizing ApiController validation responses.
public class ValidationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(x => x.Value.Errors.Count > 0)
.Select(x => new { Field = x.Key, Errors = x.Value.Errors.Select(e => e.ErrorMessage) })
.ToList();
context.Result = new BadRequestObjectResult(new { Errors = errors });
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
[ServiceFilter(typeof(ValidationFilter))]
public class UsersController : ControllerBase
{
// Controller methods...
}
Reference: ASP.NET Core web API validation.
48. Custom Middleware
A monotonic stopwatch measures elapsed time. Register the middleware in the application pipeline. If an exception escapes, this log may run before an outer exception handler sets the final status; place middleware thoughtfully or log completion separately.
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Request {Method} {Path} completed in {Duration}ms with status {StatusCode}",
context.Request.Method,
context.Request.Path,
stopwatch.Elapsed.TotalMilliseconds,
context.Response.StatusCode);
}
}
}
// Extension method
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
49. API Versioning
Uses the Asp.Versioning.Mvc package with the Asp.Versioning namespace. Register AddApiVersioning().AddMvc() alongside controllers. These action bodies are routing sketches; replace empty Ok responses with data, and remove async if no await is used.
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<User>>> GetUsersV1()
{
// Version 1.0 implementation
return Ok();
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsersV2()
{
// Version 2.0 implementation with DTOs
return Ok();
}
}
Reference: ASP.NET API Versioning.
50. Response Caching
ResponseCache emits HTTP caching policy; storage depends on a compliant cache and response eligibility. VaryByQueryKeys is for query strings, while a route id is already part of the URL. Only cache public, identity-independent responses with Location.Any.
// Program.cs, before builder.Build():
builder.Services.AddControllers();
builder.Services.AddResponseCaching();
// After builder.Build(), before mapping endpoints:
app.UseResponseCaching();
app.MapControllers();
// Controller example (separate file):
[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase
{
[HttpGet("{id:int}")]
[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Any)]
public ActionResult<object> GetProduct(int id) =>
Ok(new { Id = id, Name = "Public demonstration product" });
}
Reference: ASP.NET Core response caching.
51. Memory Pool Usage
ArrayPool may return a larger array containing old data. Process only bytesRead, complete asynchronous use before returning the array, and use Return(buffer, clearArray: true) for sensitive contents. Supply ProcessBufferAsync and propagate cancellation as needed.
public class BufferManager
{
private readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
public async Task ProcessDataAsync(Stream input)
{
var buffer = _pool.Rent(4096);
try
{
int bytesRead;
while ((bytesRead = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// Process the data
await ProcessBufferAsync(buffer, bytesRead);
}
}
finally
{
_pool.Return(buffer);
}
}
}
52. Object Pooling
This educational pool has no capacity bound or disposal policy. Return each object once and reset its state before returning it. The concurrent bag protects the pool, not an object after checkout. Consider Microsoft.Extensions.ObjectPool for a reusable implementation.
public class ObjectPool<T> where T : class
{
private readonly ConcurrentBag<T> _objects;
private readonly Func<T> _objectGenerator;
public ObjectPool(Func<T> objectGenerator)
{
_objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator));
_objects = new ConcurrentBag<T>();
}
public T Get() => _objects.TryTake(out T item) ? item : _objectGenerator();
public void Return(T item)
{
if (item != null)
{
_objects.Add(item);
}
}
}
// Usage
var pool = new ObjectPool<StringBuilder>(() => new StringBuilder());
var sb = pool.Get();
try
{
sb.Append("Hello World");
// Use StringBuilder
}
finally
{
sb.Clear();
pool.Return(sb);
}
53. Lazy Loading
Lazy<T> defers construction and normally caches the value. The empty LoadOrders method is a placeholder; this is not automatic EF lazy loading. Database-backed lazy loading can cause N+1 queries and lifetime problems.
public class User
{
private readonly Lazy<IEnumerable<Order>> _orders;
public User()
{
_orders = new Lazy<IEnumerable<Order>>(() => LoadOrders());
}
public IEnumerable<Order> Orders => _orders.Value;
private IEnumerable<Order> LoadOrders()
{
// Load orders from database
return new List<Order>();
}
}
54. Weak References
A weak reference allows collection of a cached value when no strong references keep it alive. This simple synchronized design avoids the original cleanup race, but executes the factory under a global lock: it must be short and must not reenter the cache. Keys remain until cleanup.
public sealed class WeakCache<TKey, TValue>
where TKey : notnull where TValue : class
{
private readonly object _gate = new();
private readonly Dictionary<TKey, WeakReference<TValue>> _items = new();
public TValue GetOrAdd(TKey key, Func<TKey, TValue> factory)
{
lock (_gate)
{
if (_items.TryGetValue(key, out var reference) &&
reference.TryGetTarget(out var existing)) return existing;
var value = factory(key) ?? throw new InvalidOperationException("Null cache value.");
_items[key] = new WeakReference<TValue>(value);
return value;
}
}
public void Cleanup()
{
lock (_gate)
{
var deadKeys = _items.Where(p => !p.Value.TryGetTarget(out _))
.Select(p => p.Key).ToArray();
foreach (var key in deadKeys) _items.Remove(key);
}
}
}
55. Value Types vs Reference Types
Struct assignments copy values; class assignments copy references. A property returning a struct returns a copy, so update a local value and assign it back. Reference-type fields inside structs are still shallowly copied.
// Value type (struct)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
// Reference type (class)
public class Rectangle
{
public Point TopLeft { get; set; }
public Point BottomRight { get; set; }
public Rectangle(Point topLeft, Point bottomRight)
{
TopLeft = topLeft;
BottomRight = bottomRight;
}
}
// Usage
var point1 = new Point(10, 20);
var point2 = point1; // Copy
point2.X = 30; // point1.X is still 10
var rect1 = new Rectangle(new Point(0, 0), new Point(100, 100));
var rect2 = rect1; // Reference
var topLeft = rect2.TopLeft; // Property getter returns a struct copy
topLeft.X = 50;
rect2.TopLeft = topLeft; // Both rect1 and rect2 refer to this same Rectangle
Reference: Compiler error CS1612.
56. Unit Test with Moq
Requires MSTest, Moq, User, and the service/contracts from question 31. Task-returning methods have no result object; configure completed tasks and verify the calls. The exception test expects persistence failure to prevent email.
[TestClass]
public class UserServiceTests
{
private Mock<IUserRepository> _mockRepository;
private Mock<IEmailService> _mockEmailService;
private UserService _userService;
[TestInitialize]
public void Setup()
{
_mockRepository = new Mock<IUserRepository>();
_mockEmailService = new Mock<IEmailService>();
_userService = new UserService(_mockRepository.Object, _mockEmailService.Object);
}
[TestMethod]
public async Task CreateUser_ValidUser_ShouldCreateAndSendEmail()
{
// Arrange
var user = new User { Name = "John", Email = "john@example.com" };
_mockRepository.Setup(r => r.AddAsync(It.IsAny<User>())).Returns(Task.CompletedTask);
_mockEmailService.Setup(e => e.SendWelcomeEmailAsync(user.Email))
.Returns(Task.CompletedTask);
// Act
await _userService.CreateUserAsync(user);
// Assert
_mockRepository.Verify(r => r.AddAsync(It.IsAny<User>()), Times.Once);
_mockEmailService.Verify(e => e.SendWelcomeEmailAsync(user.Email), Times.Once);
// CreateUserAsync returns Task, so there is no result value.
}
[TestMethod]
public async Task CreateUser_RepositoryThrowsException_ShouldPropagateException()
{
// Arrange
var user = new User { Name = "John", Email = "john@example.com" };
_mockRepository.Setup(r => r.AddAsync(It.IsAny<User>()))
.ThrowsAsync(new InvalidOperationException("Database error"));
// Act & Assert
await Assert.ThrowsExceptionAsync<InvalidOperationException>(
() => _userService.CreateUserAsync(user));
}
}
57. Integration Test
Requires Microsoft.AspNetCore.Mvc.Testing, MSTest, the application entry point, and the named test doubles. Expose Program if necessary for WebApplicationFactory. Replace relevant registrations, isolate the database, and add assertions on seeded results; non-null deserialization alone is weak coverage.
[TestClass]
public class UserControllerIntegrationTests
{
private WebApplicationFactory<Program> _factory;
private HttpClient _client;
[TestInitialize]
public void Setup()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real services with test doubles
services.AddScoped<IUserRepository, InMemoryUserRepository>();
services.AddScoped<IEmailService, MockEmailService>();
});
});
_client = _factory.CreateClient();
}
[TestMethod]
public async Task GetUsers_ShouldReturnUsers()
{
// Act
var response = await _client.GetAsync("/api/users");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var users = JsonSerializer.Deserialize<List<User>>(content, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.IsNotNull(users);
}
[TestCleanup]
public void Cleanup()
{
_client?.Dispose();
_factory?.Dispose();
}
}
58. Test Data Builders
A builder supplies readable defaults and overrides for test data. Fixed dates are preferable for deterministic tests. The User type needs the properties shown in question 41.
public class UserBuilder
{
private string _name = "John Doe";
private string _email = "john@example.com";
private DateTime _createdAt = DateTime.UtcNow;
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
public UserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public UserBuilder WithCreatedAt(DateTime createdAt)
{
_createdAt = createdAt;
return this;
}
public User Build()
{
return new User
{
Name = _name,
Email = _email,
CreatedAt = _createdAt
};
}
}
// Usage in tests
[TestMethod]
public void TestWithUserBuilder()
{
var user = new UserBuilder()
.WithName("Jane Doe")
.WithEmail("jane@example.com")
.Build();
Assert.AreEqual("Jane Doe", user.Name);
Assert.AreEqual("jane@example.com", user.Email);
}
59. Test Categories
Categories select or organize tests; they do not make a test fast or realistic. A real integration test needs its dependencies and assertions. A performance test needs measured workloads and criteria, not only a Performance label.
[TestClass]
public sealed class StringAlgorithmTests
{
[TestMethod]
[TestCategory("Unit")]
[TestCategory("Fast")]
public void ReverseString_PreservesEmoji()
{
Assert.AreEqual("B😀A", Algorithms.ReverseString("A😀B"));
}
}
// Algorithms is a static class containing the method from question 1.
// Run this category with: dotnet test --filter TestCategory=Unit