Skills · Coding

.NET Backend Development Patterns

Unverified25/40

Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.

Originally by wshobson · MIT

Claude CodePartialHas SKILL.md but declares no allowed-tools — Claude Code will ask for permission each time
CursorPartialPlain prose you can paste in — but no Cursor rules file
CodexPartialPlain prose you can paste in — but no AGENTS.md
Gemini CLIPartialPlain prose you can paste in
CopilotPartialPlain prose you can paste in — but no Copilot instructions file
npx agentalley add dotnet-backend-patterns

This command does not work yet — the CLI is still being built. Until then, use Raw in the reader below to take the file.

Who is stuck, and on what

Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.

The whole source

No sign-in, no blur, nothing truncated
dotnet-backend-patterns/SKILL.md811 lines25.8 KBRawView on GitHub
Frontmatter — 2 properties
namedotnet-backend-patterns
descriptionMaster C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.
1---
2name: dotnet-backend-patterns
3description: Master C#/.NET backend development patterns for building robust APIs, MCP servers, and enterprise applications. Covers async/await, dependency injection, Entity Framework Core, Dapper, configuration, caching, and testing with xUnit. Use when developing .NET backends, reviewing C# code, or designing API architectures.
4---A5No allowed-tools declared — no way to tell what this skill may touch
5 
6# .NET Backend Development Patterns
7 
8Master C#/.NET patterns for building production-grade APIs, MCP servers, and enterprise backends with modern best practices (2024/2025).
9 
10## When to Use This Skill
11 
12- Developing new .NET Web APIs or MCP servers
13- Reviewing C# code for quality and performance
14- Designing service architectures with dependency injection
15- Implementing caching strategies with Redis
16- Writing unit and integration tests
17- Optimizing database access with EF Core or Dapper
18- Configuring applications with IOptions pattern
19- Handling errors and implementing resilience patterns
20 
21## Core Concepts
22 
23### 1. Project Structure (Clean Architecture)
24 
25```
26src/
27├── Domain/ # Core business logic (no dependencies)
28│ ├── Entities/
29│ ├── Interfaces/
30│ ├── Exceptions/
31│ └── ValueObjects/
32├── Application/ # Use cases, DTOs, validation
33│ ├── Services/
34│ ├── DTOs/
35│ ├── Validators/
36│ └── Interfaces/
37├── Infrastructure/ # External implementations
38│ ├── Data/ # EF Core, Dapper repositories
39│ ├── Caching/ # Redis, Memory cache
40│ ├── External/ # HTTP clients, third-party APIs
41│ └── DependencyInjection/ # Service registration
42└── Api/ # Entry point
43 ├── Controllers/ # Or MinimalAPI endpoints
44 ├── Middleware/
45 ├── Filters/
46 └── Program.cs
47```
48 
49### 2. Dependency Injection Patterns
50 
51```csharp
52// Service registration by lifetime
53public static class ServiceCollectionExtensions
54{
55 public static IServiceCollection AddApplicationServices(
56 this IServiceCollection services,
57 IConfiguration configuration)
58 {
59 // Scoped: One instance per HTTP request
60 services.AddScoped<IProductService, ProductService>();
61 services.AddScoped<IOrderService, OrderService>();
62 
63 // Singleton: One instance for app lifetime
64 services.AddSingleton<ICacheService, RedisCacheService>();
65 services.AddSingleton<IConnectionMultiplexer>(_ =>
66 ConnectionMultiplexer.Connect(configuration["Redis:Connection"]!));
67 
68 // Transient: New instance every time
69 services.AddTransient<IValidator<CreateOrderRequest>, CreateOrderValidator>();
70 
71 // Options pattern for configuration
72 services.Configure<CatalogOptions>(configuration.GetSection("Catalog"));
73 services.Configure<RedisOptions>(configuration.GetSection("Redis"));
74 
75 // Factory pattern for conditional creation
76 services.AddScoped<IPriceCalculator>(sp =>
77 {
78 var options = sp.GetRequiredService<IOptions<PricingOptions>>().Value;
79 return options.UseNewEngine
80 ? sp.GetRequiredService<NewPriceCalculator>()
81 : sp.GetRequiredService<LegacyPriceCalculator>();
82 });
83 
84 // Keyed services (.NET 8+)
85 services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
86 services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
87 
88 return services;
89 }
90}
91 
92// Usage with keyed services
93public class CheckoutService
94{
95 public CheckoutService(
96 [FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor)
97 {
98 _processor = stripeProcessor;
99 }
100}
101```
102 
103### 3. Async/Await Patterns
104 
105```csharp
106// ✅ CORRECT: Async all the way down
107public async Task<Product> GetProductAsync(string id, CancellationToken ct = default)
108{
109 return await _repository.GetByIdAsync(id, ct);
110}
111 
112// ✅ CORRECT: Parallel execution with WhenAll
113public async Task<(Stock, Price)> GetStockAndPriceAsync(
114 string productId,
115 CancellationToken ct = default)
116{
117 var stockTask = _stockService.GetAsync(productId, ct);
118 var priceTask = _priceService.GetAsync(productId, ct);
119 
120 await Task.WhenAll(stockTask, priceTask);
121 
122 return (await stockTask, await priceTask);
123}
124 
125// ✅ CORRECT: ConfigureAwait in libraries
126public async Task<T> LibraryMethodAsync<T>(CancellationToken ct = default)
127{
128 var result = await _httpClient.GetAsync(url, ct).ConfigureAwait(false);
129 return await result.Content.ReadFromJsonAsync<T>(ct).ConfigureAwait(false);
130}
131 
132// ✅ CORRECT: ValueTask for hot paths with caching
133public ValueTask<Product?> GetCachedProductAsync(string id)
134{
135 if (_cache.TryGetValue(id, out Product? product))
136 return ValueTask.FromResult(product);
137 
138 return new ValueTask<Product?>(GetFromDatabaseAsync(id));
139}
140 
141// ❌ WRONG: Blocking on async (deadlock risk)
142var result = GetProductAsync(id).Result; // NEVER do this
143var result2 = GetProductAsync(id).GetAwaiter().GetResult(); // Also bad
144 
145// ❌ WRONG: async void (except event handlers)
146public async void ProcessOrder() { } // Exceptions are lost
147 
148// ❌ WRONG: Unnecessary Task.Run for already async code
149await Task.Run(async () => await GetDataAsync()); // Wastes thread
150```
151 
152### 4. Configuration with IOptions
153 
154```csharp
155// Configuration classes
156public class CatalogOptions
157{
158 public const string SectionName = "Catalog";
159 
160 public int DefaultPageSize { get; set; } = 50;
161 public int MaxPageSize { get; set; } = 200;
162 public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(15);
163 public bool EnableEnrichment { get; set; } = true;
164}
165 
166public class RedisOptions
167{
168 public const string SectionName = "Redis";
169 
170 public string Connection { get; set; } = "localhost:6379";
171 public string KeyPrefix { get; set; } = "mcp:";
172 public int Database { get; set; } = 0;
173}
174 
175// appsettings.json
176{
177 "Catalog": {
178 "DefaultPageSize": 50,
179 "MaxPageSize": 200,
180 "CacheDuration": "00:15:00",
181 "EnableEnrichment": true
182 },
183 "Redis": {
184 "Connection": "localhost:6379",
185 "KeyPrefix": "mcp:",
186 "Database": 0
187 }
188}
189 
190// Registration
191services.Configure<CatalogOptions>(configuration.GetSection(CatalogOptions.SectionName));
192services.Configure<RedisOptions>(configuration.GetSection(RedisOptions.SectionName));
193 
194// Usage with IOptions (singleton, read once at startup)
195public class CatalogService
196{
197 private readonly CatalogOptions _options;
198 
199 public CatalogService(IOptions<CatalogOptions> options)
200 {
201 _options = options.Value;
202 }
203}
204 
205// Usage with IOptionsSnapshot (scoped, re-reads on each request)
206public class DynamicService
207{
208 private readonly CatalogOptions _options;
209 
210 public DynamicService(IOptionsSnapshot<CatalogOptions> options)
211 {
212 _options = options.Value; // Fresh value per request
213 }
214}
215 
216// Usage with IOptionsMonitor (singleton, notified on changes)
217public class MonitoredService
218{
219 private CatalogOptions _options;
220 
221 public MonitoredService(IOptionsMonitor<CatalogOptions> monitor)
222 {
223 _options = monitor.CurrentValue;
224 monitor.OnChange(newOptions => _options = newOptions);
225 }
226}
227```
228 
229### 5. Result Pattern (Avoiding Exceptions for Flow Control)
230 
231```csharp
232// Generic Result type
233public class Result<T>
234{
235 public bool IsSuccess { get; }
236 public T? Value { get; }
237 public string? Error { get; }
238 public string? ErrorCode { get; }
239 
240 private Result(bool isSuccess, T? value, string? error, string? errorCode)
241 {
242 IsSuccess = isSuccess;
243 Value = value;
244 Error = error;
245 ErrorCode = errorCode;
246 }
247 
248 public static Result<T> Success(T value) => new(true, value, null, null);
249 public static Result<T> Failure(string error, string? code = null) => new(false, default, error, code);
250 
251 public Result<TNew> Map<TNew>(Func<T, TNew> mapper) =>
252 IsSuccess ? Result<TNew>.Success(mapper(Value!)) : Result<TNew>.Failure(Error!, ErrorCode);
253 
254 public async Task<Result<TNew>> MapAsync<TNew>(Func<T, Task<TNew>> mapper) =>
255 IsSuccess ? Result<TNew>.Success(await mapper(Value!)) : Result<TNew>.Failure(Error!, ErrorCode);
256}
257 
258// Usage in service
259public async Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
260{
261 // Validation
262 var validation = await _validator.ValidateAsync(request, ct);
263 if (!validation.IsValid)
264 return Result<Order>.Failure(
265 validation.Errors.First().ErrorMessage,
266 "VALIDATION_ERROR");
267 
268 // Business rule check
269 var stock = await _stockService.CheckAsync(request.ProductId, request.Quantity, ct);
270 if (!stock.IsAvailable)
271 return Result<Order>.Failure(
272 $"Insufficient stock: {stock.Available} available, {request.Quantity} requested",
273 "INSUFFICIENT_STOCK");
274 
275 // Create order
276 var order = await _repository.CreateAsync(request.ToEntity(), ct);
277 
278 return Result<Order>.Success(order);
279}
280 
281// Usage in controller/endpoint
282app.MapPost("/orders", async (
283 CreateOrderRequest request,
284 IOrderService orderService,
285 CancellationToken ct) =>
286{
287 var result = await orderService.CreateOrderAsync(request, ct);
288 
289 return result.IsSuccess
290 ? Results.Created($"/orders/{result.Value!.Id}", result.Value)
291 : Results.BadRequest(new { error = result.Error, code = result.ErrorCode });
292});
293```
294 
295## Data Access Patterns
296 
297### Entity Framework Core
298 
299```csharp
300// DbContext configuration
301public class AppDbContext : DbContext
302{
303 public DbSet<Product> Products => Set<Product>();
304 public DbSet<Order> Orders => Set<Order>();
305 
306 protected override void OnModelCreating(ModelBuilder modelBuilder)
307 {
308 // Apply all configurations from assembly
309 modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
310 
311 // Global query filters
312 modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);
313 }
314}
315 
316// Entity configuration
317public class ProductConfiguration : IEntityTypeConfiguration<Product>
318{
319 public void Configure(EntityTypeBuilder<Product> builder)
320 {
321 builder.ToTable("Products");
322 
323 builder.HasKey(p => p.Id);
324 builder.Property(p => p.Id).HasMaxLength(40);
325 builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
326 builder.Property(p => p.Price).HasPrecision(18, 2);
327 
328 builder.HasIndex(p => p.Sku).IsUnique();
329 builder.HasIndex(p => new { p.CategoryId, p.Name });
330 
331 builder.HasMany(p => p.OrderItems)
332 .WithOne(oi => oi.Product)
333 .HasForeignKey(oi => oi.ProductId);
334 }
335}
336 
337// Repository with EF Core
338public class ProductRepository : IProductRepository
339{
340 private readonly AppDbContext _context;
341 
342 public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
343 {
344 return await _context.Products
345 .AsNoTracking()
346 .FirstOrDefaultAsync(p => p.Id == id, ct);
347 }
348 
349 public async Task<IReadOnlyList<Product>> SearchAsync(
350 ProductSearchCriteria criteria,
351 CancellationToken ct = default)
352 {
353 var query = _context.Products.AsNoTracking();
354 
355 if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))
356 query = query.Where(p => EF.Functions.Like(p.Name, $"%{criteria.SearchTerm}%"));
357 
358 if (criteria.CategoryId.HasValue)
359 query = query.Where(p => p.CategoryId == criteria.CategoryId);
360 
361 if (criteria.MinPrice.HasValue)
362 query = query.Where(p => p.Price >= criteria.MinPrice);
363 
364 if (criteria.MaxPrice.HasValue)
365 query = query.Where(p => p.Price <= criteria.MaxPrice);
366 
367 return await query
368 .OrderBy(p => p.Name)
369 .Skip((criteria.Page - 1) * criteria.PageSize)
370 .Take(criteria.PageSize)
371 .ToListAsync(ct);
372 }
373}
374```
375 
376### Dapper for Performance
377 
378```csharp
379public class DapperProductRepository : IProductRepository
380{
381 private readonly IDbConnection _connection;
382 
383 public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
384 {
385 const string sql = """
386 SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt
387 FROM Products
388 WHERE Id = @Id AND IsDeleted = 0
389 """;
390 
391 return await _connection.QueryFirstOrDefaultAsync<Product>(
392 new CommandDefinition(sql, new { Id = id }, cancellationToken: ct));
393 }
394 
395 public async Task<IReadOnlyList<Product>> SearchAsync(
396 ProductSearchCriteria criteria,
397 CancellationToken ct = default)
398 {
399 var sql = new StringBuilder("""
400 SELECT Id, Name, Sku, Price, CategoryId, Stock, CreatedAt
401 FROM Products
402 WHERE IsDeleted = 0
403 """);
404 
405 var parameters = new DynamicParameters();
406 
407 if (!string.IsNullOrWhiteSpace(criteria.SearchTerm))
408 {
409 sql.Append(" AND Name LIKE @SearchTerm");
410 parameters.Add("SearchTerm", $"%{criteria.SearchTerm}%");
411 }
412 
413 if (criteria.CategoryId.HasValue)
414 {
415 sql.Append(" AND CategoryId = @CategoryId");
416 parameters.Add("CategoryId", criteria.CategoryId);
417 }
418 
419 if (criteria.MinPrice.HasValue)
420 {
421 sql.Append(" AND Price >= @MinPrice");
422 parameters.Add("MinPrice", criteria.MinPrice);
423 }
424 
425 if (criteria.MaxPrice.HasValue)
426 {
427 sql.Append(" AND Price <= @MaxPrice");
428 parameters.Add("MaxPrice", criteria.MaxPrice);
429 }
430 
431 sql.Append(" ORDER BY Name OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY");A4This skill pulls in web or user content but never says to treat that content as data. A signal, not proof.
432 parameters.Add("Offset", (criteria.Page - 1) * criteria.PageSize);
433 parameters.Add("PageSize", criteria.PageSize);
434 
435 var results = await _connection.QueryAsync<Product>(
436 new CommandDefinition(sql.ToString(), parameters, cancellationToken: ct));
437 
438 return results.ToList();
439 }
440 
441 // Multi-mapping for related data
442 public async Task<Order?> GetOrderWithItemsAsync(int orderId, CancellationToken ct = default)
443 {
444 const string sql = """
445 SELECT o.*, oi.*, p.*
446 FROM Orders o
447 LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
448 LEFT JOIN Products p ON oi.ProductId = p.Id
449 WHERE o.Id = @OrderId
450 """;
451 
452 var orderDictionary = new Dictionary<int, Order>();
453 
454 await _connection.QueryAsync<Order, OrderItem, Product, Order>(
455 new CommandDefinition(sql, new { OrderId = orderId }, cancellationToken: ct),
456 (order, item, product) =>
457 {
458 if (!orderDictionary.TryGetValue(order.Id, out var existingOrder))
459 {
460 existingOrder = order;
461 existingOrder.Items = new List<OrderItem>();
462 orderDictionary.Add(order.Id, existingOrder);
463 }
464 
465 if (item != null)
466 {
467 item.Product = product;
468 existingOrder.Items.Add(item);
469 }
470 
471 return existingOrder;
472 },
473 splitOn: "Id,Id");
474 
475 return orderDictionary.Values.FirstOrDefault();
476 }
477}
478```
479 
480## Caching Patterns
481 
482### Multi-Level Cache with Redis
483 
484```csharp
485public class CachedProductService : IProductService
486{
487 private readonly IProductRepository _repository;
488 private readonly IMemoryCache _memoryCache;
489 private readonly IDistributedCache _distributedCache;
490 private readonly ILogger<CachedProductService> _logger;
491 
492 private static readonly TimeSpan MemoryCacheDuration = TimeSpan.FromMinutes(1);
493 private static readonly TimeSpan DistributedCacheDuration = TimeSpan.FromMinutes(15);
494 
495 public async Task<Product?> GetByIdAsync(string id, CancellationToken ct = default)
496 {
497 var cacheKey = $"product:{id}";
498 
499 // L1: Memory cache (in-process, fastest)
500 if (_memoryCache.TryGetValue(cacheKey, out Product? cached))
501 {
502 _logger.LogDebug("L1 cache hit for {CacheKey}", cacheKey);
503 return cached;
504 }
505 
506 // L2: Distributed cache (Redis)
507 var distributed = await _distributedCache.GetStringAsync(cacheKey, ct);
508 if (distributed != null)
509 {
510 _logger.LogDebug("L2 cache hit for {CacheKey}", cacheKey);
511 var product = JsonSerializer.Deserialize<Product>(distributed);
512 
513 // Populate L1
514 _memoryCache.Set(cacheKey, product, MemoryCacheDuration);
515 return product;
516 }
517 
518 // L3: Database
519 _logger.LogDebug("Cache miss for {CacheKey}, fetching from database", cacheKey);
520 var fromDb = await _repository.GetByIdAsync(id, ct);
521 
522 if (fromDb != null)
523 {
524 var serialized = JsonSerializer.Serialize(fromDb);
525 
526 // Populate both caches
527 await _distributedCache.SetStringAsync(
528 cacheKey,
529 serialized,
530 new DistributedCacheEntryOptions
531 {
532 AbsoluteExpirationRelativeToNow = DistributedCacheDuration
533 },
534 ct);
535 
536 _memoryCache.Set(cacheKey, fromDb, MemoryCacheDuration);
537 }
538 
539 return fromDb;
540 }
541 
542 public async Task InvalidateAsync(string id, CancellationToken ct = default)
543 {
544 var cacheKey = $"product:{id}";
545 
546 _memoryCache.Remove(cacheKey);
547 await _distributedCache.RemoveAsync(cacheKey, ct);
548 
549 _logger.LogInformation("Invalidated cache for {CacheKey}", cacheKey);
550 }
551}
552 
553// Stale-while-revalidate pattern
554public class StaleWhileRevalidateCache<T>
555{
556 private readonly IDistributedCache _cache;
557 private readonly TimeSpan _freshDuration;
558 private readonly TimeSpan _staleDuration;
559 
560 public async Task<T?> GetOrCreateAsync(
561 string key,
562 Func<CancellationToken, Task<T>> factory,
563 CancellationToken ct = default)
564 {
565 var cached = await _cache.GetStringAsync(key, ct);
566 
567 if (cached != null)
568 {
569 var entry = JsonSerializer.Deserialize<CacheEntry<T>>(cached)!;
570 
571 if (entry.IsStale && !entry.IsExpired)
572 {
573 // Return stale data immediately, refresh in background
574 _ = Task.Run(async () =>
575 {
576 var fresh = await factory(CancellationToken.None);
577 await SetAsync(key, fresh, CancellationToken.None);
578 });
579 }
580 
581 if (!entry.IsExpired)
582 return entry.Value;
583 }
584 
585 // Cache miss or expired
586 var value = await factory(ct);
587 await SetAsync(key, value, ct);
588 return value;
589 }
590 
591 private record CacheEntry<TValue>(TValue Value, DateTime CreatedAt)
592 {
593 public bool IsStale => DateTime.UtcNow - CreatedAt > _freshDuration;
594 public bool IsExpired => DateTime.UtcNow - CreatedAt > _staleDuration;
595 }
596}
597```
598 
599## Testing Patterns
600 
601### Unit Tests with xUnit and Moq
602 
603```csharp
604public class OrderServiceTests
605{
606 private readonly Mock<IOrderRepository> _mockRepository;
607 private readonly Mock<IStockService> _mockStockService;
608 private readonly Mock<IValidator<CreateOrderRequest>> _mockValidator;
609 private readonly OrderService _sut; // System Under Test
610 
611 public OrderServiceTests()
612 {
613 _mockRepository = new Mock<IOrderRepository>();
614 _mockStockService = new Mock<IStockService>();
615 _mockValidator = new Mock<IValidator<CreateOrderRequest>>();
616 
617 // Default: validation passes
618 _mockValidator
619 .Setup(v => v.ValidateAsync(It.IsAny<CreateOrderRequest>(), It.IsAny<CancellationToken>()))
620 .ReturnsAsync(new ValidationResult());
621 
622 _sut = new OrderService(
623 _mockRepository.Object,
624 _mockStockService.Object,
625 _mockValidator.Object);
626 }
627 
628 [Fact]
629 public async Task CreateOrderAsync_WithValidRequest_ReturnsSuccess()
630 {
631 // Arrange
632 var request = new CreateOrderRequest
633 {
634 ProductId = "PROD-001",
635 Quantity = 5,
636 CustomerOrderCode = "ORD-2024-001"
637 };
638 
639 _mockStockService
640 .Setup(s => s.CheckAsync("PROD-001", 5, It.IsAny<CancellationToken>()))
641 .ReturnsAsync(new StockResult { IsAvailable = true, Available = 10 });
642 
643 _mockRepository
644 .Setup(r => r.CreateAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()))
645 .ReturnsAsync(new Order { Id = 1, CustomerOrderCode = "ORD-2024-001" });
646 
647 // Act
648 var result = await _sut.CreateOrderAsync(request);
649 
650 // Assert
651 Assert.True(result.IsSuccess);
652 Assert.NotNull(result.Value);
653 Assert.Equal(1, result.Value.Id);
654 
655 _mockRepository.Verify(
656 r => r.CreateAsync(It.Is<Order>(o => o.CustomerOrderCode == "ORD-2024-001"),
657 It.IsAny<CancellationToken>()),
658 Times.Once);
659 }
660 
661 [Fact]
662 public async Task CreateOrderAsync_WithInsufficientStock_ReturnsFailure()
663 {
664 // Arrange
665 var request = new CreateOrderRequest { ProductId = "PROD-001", Quantity = 100 };
666 
667 _mockStockService
668 .Setup(s => s.CheckAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
669 .ReturnsAsync(new StockResult { IsAvailable = false, Available = 5 });
670 
671 // Act
672 var result = await _sut.CreateOrderAsync(request);
673 
674 // Assert
675 Assert.False(result.IsSuccess);
676 Assert.Equal("INSUFFICIENT_STOCK", result.ErrorCode);
677 Assert.Contains("5 available", result.Error);
678 
679 _mockRepository.Verify(
680 r => r.CreateAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()),
681 Times.Never);
682 }
683 
684 [Theory]
685 [InlineData(0)]
686 [InlineData(-1)]
687 [InlineData(-100)]
688 public async Task CreateOrderAsync_WithInvalidQuantity_ReturnsValidationError(int quantity)
689 {
690 // Arrange
691 var request = new CreateOrderRequest { ProductId = "PROD-001", Quantity = quantity };
692 
693 _mockValidator
694 .Setup(v => v.ValidateAsync(request, It.IsAny<CancellationToken>()))
695 .ReturnsAsync(new ValidationResult(new[]
696 {
697 new ValidationFailure("Quantity", "Quantity must be greater than 0")
698 }));
699 
700 // Act
701 var result = await _sut.CreateOrderAsync(request);
702 
703 // Assert
704 Assert.False(result.IsSuccess);
705 Assert.Equal("VALIDATION_ERROR", result.ErrorCode);
706 }
707}
708```
709 
710### Integration Tests with WebApplicationFactory
711 
712```csharp
713public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
714{
715 private readonly WebApplicationFactory<Program> _factory;
716 private readonly HttpClient _client;
717 
718 public ProductsApiTests(WebApplicationFactory<Program> factory)
719 {
720 _factory = factory.WithWebHostBuilder(builder =>
721 {
722 builder.ConfigureServices(services =>
723 {
724 // Replace real database with in-memory
725 services.RemoveAll<DbContextOptions<AppDbContext>>();
726 services.AddDbContext<AppDbContext>(options =>
727 options.UseInMemoryDatabase("TestDb"));
728 
729 // Replace Redis with memory cache
730 services.RemoveAll<IDistributedCache>();
731 services.AddDistributedMemoryCache();
732 });
733 });
734 
735 _client = _factory.CreateClient();
736 }
737 
738 [Fact]
739 public async Task GetProduct_WithValidId_ReturnsProduct()
740 {
741 // Arrange
742 using var scope = _factory.Services.CreateScope();
743 var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
744 
745 context.Products.Add(new Product
746 {
747 Id = "TEST-001",
748 Name = "Test Product",
749 Price = 99.99m
750 });
751 await context.SaveChangesAsync();
752 
753 // Act
754 var response = await _client.GetAsync("/api/products/TEST-001");
755 
756 // Assert
757 response.EnsureSuccessStatusCode();
758 var product = await response.Content.ReadFromJsonAsync<Product>();
759 Assert.Equal("Test Product", product!.Name);
760 }
761 
762 [Fact]
763 public async Task GetProduct_WithInvalidId_Returns404()A335-character high-entropy string — looks like a pasted key
764 {
765 // Act
766 var response = await _client.GetAsync("/api/products/NONEXISTENT");
767 
768 // Assert
769 Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
770 }
771}
772```
773 
774## Best Practices
775 
776### DO
777 
7781. **Use async/await** all the way through the call stack
7792. **Inject dependencies** through constructor injection
7803. **Use IOptions<T>** for typed configuration
7814. **Return Result types** instead of throwing exceptions for business logic
7825. **Use CancellationToken** in all async methods
7836. **Prefer Dapper** for read-heavy, performance-critical queries
7847. **Use EF Core** for complex domain models with change tracking
7858. **Cache aggressively** with proper invalidation strategies
7869. **Write unit tests** for business logic, integration tests for APIs
78710. **Use record types** for DTOs and immutable data
788 
789### DON'T
790 
7911. **Don't block on async** with `.Result` or `.Wait()`
7922. **Don't use async void** except for event handlers
7933. **Don't catch generic Exception** without re-throwing or logging
7944. **Don't hardcode** configuration values
7955. **Don't expose EF entities** directly in APIs (use DTOs)
7966. **Don't forget** `AsNoTracking()` for read-only queries
7977. **Don't ignore** CancellationToken parameters
7988. **Don't create** `new HttpClient()` manually (use IHttpClientFactory)
7999. **Don't mix** sync and async code unnecessarily
80010. **Don't skip** validation at API boundaries
801 
802## Common Pitfalls
803 
804- **N+1 Queries**: Use `.Include()` or explicit joins
805- **Memory Leaks**: Dispose IDisposable resources, use `using`
806- **Deadlocks**: Don't mix sync and async, use ConfigureAwait(false) in libraries
807- **Over-fetching**: Select only needed columns, use projections
808- **Missing Indexes**: Check query plans, add indexes for common filters
809- **Timeout Issues**: Configure appropriate timeouts for HTTP clients
810- **Cache Stampede**: Use distributed locks for cache population
811 

Reviews

Installed this one?Write the first review and take the Trailblazer badge.

Reviews only open after a real install, so this is empty — and we leave it empty rather than invent one.

Alternatives

Also in Coding