安装
npx skills add https://github.com/wshobson/agents --skill dotnet-backend-patterns
- .NET Backend Development Patterns
- Master C#/.NET patterns for building production-grade APIs, MCP servers, and enterprise backends with modern best practices (2024/2025).
- When to Use This Skill
- Developing new .NET Web APIs or MCP servers
- Reviewing C# code for quality and performance
- Designing service architectures with dependency injection
- Implementing caching strategies with Redis
- Writing unit and integration tests
- Optimizing database access with EF Core or Dapper
- Configuring applications with IOptions pattern
- Handling errors and implementing resilience patterns
- Core Concepts
- 1. Project Structure (Clean Architecture)
- src/
- ├── Domain/ # Core business logic (no dependencies)
- │ ├── Entities/
- │ ├── Interfaces/
- │ ├── Exceptions/
- │ └── ValueObjects/
- ├── Application/ # Use cases, DTOs, validation
- │ ├── Services/
- │ ├── DTOs/
- │ ├── Validators/
- │ └── Interfaces/
- ├── Infrastructure/ # External implementations
- │ ├── Data/ # EF Core, Dapper repositories
- │ ├── Caching/ # Redis, Memory cache
- │ ├── External/ # HTTP clients, third-party APIs
- │ └── DependencyInjection/ # Service registration
- └── Api/ # Entry point
- ├── Controllers/ # Or MinimalAPI endpoints
- ├── Middleware/
- ├── Filters/
- └── Program.cs
- 2. Dependency Injection Patterns
- // Service registration by lifetime
- public
- static
- class
- ServiceCollectionExtensions
- {
- public
- static
- IServiceCollection
- AddApplicationServices
- (
- this
- IServiceCollection
- services
- ,
- IConfiguration
- configuration
- )
- {
- // Scoped: One instance per HTTP request
- services
- .
- AddScoped
- <
- IProductService
- ,
- ProductService
- >
- (
- )
- ;
- services
- .
- AddScoped
- <
- IOrderService
- ,
- OrderService
- >
- (
- )
- ;
- // Singleton: One instance for app lifetime
- services
- .
- AddSingleton
- <
- ICacheService
- ,
- RedisCacheService
- >
- (
- )
- ;
- services
- .
- AddSingleton
- <
- IConnectionMultiplexer
- >
- (
- _
- =>
- ConnectionMultiplexer
- .
- Connect
- (
- configuration
- [
- "Redis:Connection"
- ]
- !
- )
- )
- ;
- // Transient: New instance every time
- services
- .
- AddTransient
- <
- IValidator
- <
- CreateOrderRequest
- >
- ,
- CreateOrderValidator
- >
- (
- )
- ;
- // Options pattern for configuration
- services
- .
- Configure
- <
- CatalogOptions
- >
- (
- configuration
- .
- GetSection
- (
- "Catalog"
- )
- )
- ;
- services
- .
- Configure
- <
- RedisOptions
- >
- (
- configuration
- .
- GetSection
- (
- "Redis"
- )
- )
- ;
- // Factory pattern for conditional creation
- services
- .
- AddScoped
- <
- IPriceCalculator
- >
- (
- sp
- =>
- {
- var
- options
- =
- sp
- .
- GetRequiredService
- <
- IOptions
- <
- PricingOptions
- >
- >
- (
- )
- .
- Value
- ;
- return
- options
- .
- UseNewEngine
- ?
- sp
- .
- GetRequiredService
- <
- NewPriceCalculator
- >
- (
- )
- :
- sp
- .
- GetRequiredService
- <
- LegacyPriceCalculator
- >
- (
- )
- ;
- }
- )
- ;
- // Keyed services (.NET 8+)
- services
- .
- AddKeyedScoped
- <
- IPaymentProcessor
- ,
- StripeProcessor
- >
- (
- "stripe"
- )
- ;
- services
- .
- AddKeyedScoped
- <
- IPaymentProcessor
- ,
- PayPalProcessor
- >
- (
- "paypal"
- )
- ;
- return
- services
- ;
- }
- }
- // Usage with keyed services
- public
- class
- CheckoutService
- {
- public
- CheckoutService
- (
- [
- FromKeyedServices
- (
- "stripe"
- )
- ]
- IPaymentProcessor
- stripeProcessor
- )
- {
- _processor
- =
- stripeProcessor
- ;
- }
- }
- 3. Async/Await Patterns
- // ✅ CORRECT: Async all the way down
- public
- async
- Task
- <
- Product
- >
- GetProductAsync
- (
- string
- id
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- return
- await
- _repository
- .
- GetByIdAsync
- (
- id
- ,
- ct
- )
- ;
- }
- // ✅ CORRECT: Parallel execution with WhenAll
- public
- async
- Task
- <
- (
- Stock
- ,
- Price
- )
- >
- GetStockAndPriceAsync
- (
- string
- productId
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- stockTask
- =
- _stockService
- .
- GetAsync
- (
- productId
- ,
- ct
- )
- ;
- var
- priceTask
- =
- _priceService
- .
- GetAsync
- (
- productId
- ,
- ct
- )
- ;
- await
- Task
- .
- WhenAll
- (
- stockTask
- ,
- priceTask
- )
- ;
- return
- (
- await
- stockTask
- ,
- await
- priceTask
- )
- ;
- }
- // ✅ CORRECT: ConfigureAwait in libraries
- public
- async
- Task
- <
- T
- >
- LibraryMethodAsync
- <
- T
- >
- (
- CancellationToken
- ct
- =
- default
- )
- {
- var
- result
- =
- await
- _httpClient
- .
- GetAsync
- (
- url
- ,
- ct
- )
- .
- ConfigureAwait
- (
- false
- )
- ;
- return
- await
- result
- .
- Content
- .
- ReadFromJsonAsync
- <
- T
- >
- (
- ct
- )
- .
- ConfigureAwait
- (
- false
- )
- ;
- }
- // ✅ CORRECT: ValueTask for hot paths with caching
- public
- ValueTask
- <
- Product
- ?
- >
- GetCachedProductAsync
- (
- string
- id
- )
- {
- if
- (
- _cache
- .
- TryGetValue
- (
- id
- ,
- out
- Product
- ?
- product
- )
- )
- return
- ValueTask
- .
- FromResult
- (
- product
- )
- ;
- return
- new
- ValueTask
- <
- Product
- ?
- >
- (
- GetFromDatabaseAsync
- (
- id
- )
- )
- ;
- }
- // ❌ WRONG: Blocking on async (deadlock risk)
- var
- result
- =
- GetProductAsync
- (
- id
- )
- .
- Result
- ;
- // NEVER do this
- var
- result2
- =
- GetProductAsync
- (
- id
- )
- .
- GetAwaiter
- (
- )
- .
- GetResult
- (
- )
- ;
- // Also bad
- // ❌ WRONG: async void (except event handlers)
- public
- async
- void
- ProcessOrder
- (
- )
- {
- }
- // Exceptions are lost
- // ❌ WRONG: Unnecessary Task.Run for already async code
- await
- Task
- .
- Run
- (
- async
- (
- )
- =>
- await
- GetDataAsync
- (
- )
- )
- ;
- // Wastes thread
- 4. Configuration with IOptions
- // Configuration classes
- public
- class
- CatalogOptions
- {
- public
- const
- string
- SectionName
- =
- "Catalog"
- ;
- public
- int
- DefaultPageSize
- {
- get
- ;
- set
- ;
- }
- =
- 50
- ;
- public
- int
- MaxPageSize
- {
- get
- ;
- set
- ;
- }
- =
- 200
- ;
- public
- TimeSpan
- CacheDuration
- {
- get
- ;
- set
- ;
- }
- =
- TimeSpan
- .
- FromMinutes
- (
- 15
- )
- ;
- public
- bool
- EnableEnrichment
- {
- get
- ;
- set
- ;
- }
- =
- true
- ;
- }
- public
- class
- RedisOptions
- {
- public
- const
- string
- SectionName
- =
- "Redis"
- ;
- public
- string
- Connection
- {
- get
- ;
- set
- ;
- }
- =
- "localhost:6379"
- ;
- public
- string
- KeyPrefix
- {
- get
- ;
- set
- ;
- }
- =
- "mcp:"
- ;
- public
- int
- Database
- {
- get
- ;
- set
- ;
- }
- =
- 0
- ;
- }
- // appsettings.json
- {
- "Catalog"
- :
- {
- "DefaultPageSize"
- :
- 50
- ,
- "MaxPageSize"
- :
- 200
- ,
- "CacheDuration"
- :
- "00:15:00"
- ,
- "EnableEnrichment"
- :
- true
- }
- ,
- "Redis"
- :
- {
- "Connection"
- :
- "localhost:6379"
- ,
- "KeyPrefix"
- :
- "mcp:"
- ,
- "Database"
- :
- 0
- }
- }
- // Registration
- services
- .
- Configure
- <
- CatalogOptions
- >
- (
- configuration
- .
- GetSection
- (
- CatalogOptions
- .
- SectionName
- )
- )
- ;
- services
- .
- Configure
- <
- RedisOptions
- >
- (
- configuration
- .
- GetSection
- (
- RedisOptions
- .
- SectionName
- )
- )
- ;
- // Usage with IOptions (singleton, read once at startup)
- public
- class
- CatalogService
- {
- private
- readonly
- CatalogOptions
- _options
- ;
- public
- CatalogService
- (
- IOptions
- <
- CatalogOptions
- >
- options
- )
- {
- _options
- =
- options
- .
- Value
- ;
- }
- }
- // Usage with IOptionsSnapshot (scoped, re-reads on each request)
- public
- class
- DynamicService
- {
- private
- readonly
- CatalogOptions
- _options
- ;
- public
- DynamicService
- (
- IOptionsSnapshot
- <
- CatalogOptions
- >
- options
- )
- {
- _options
- =
- options
- .
- Value
- ;
- // Fresh value per request
- }
- }
- // Usage with IOptionsMonitor (singleton, notified on changes)
- public
- class
- MonitoredService
- {
- private
- CatalogOptions
- _options
- ;
- public
- MonitoredService
- (
- IOptionsMonitor
- <
- CatalogOptions
- >
- monitor
- )
- {
- _options
- =
- monitor
- .
- CurrentValue
- ;
- monitor
- .
- OnChange
- (
- newOptions
- =>
- _options
- =
- newOptions
- )
- ;
- }
- }
- 5. Result Pattern (Avoiding Exceptions for Flow Control)
- // Generic Result type
- public
- class
- Result
- <
- T
- >
- {
- public
- bool
- IsSuccess
- {
- get
- ;
- }
- public
- T
- ?
- Value
- {
- get
- ;
- }
- public
- string
- ?
- Error
- {
- get
- ;
- }
- public
- string
- ?
- ErrorCode
- {
- get
- ;
- }
- private
- Result
- (
- bool
- isSuccess
- ,
- T
- ?
- value
- ,
- string
- ?
- error
- ,
- string
- ?
- errorCode
- )
- {
- IsSuccess
- =
- isSuccess
- ;
- Value
- =
- value
- ;
- Error
- =
- error
- ;
- ErrorCode
- =
- errorCode
- ;
- }
- public
- static
- Result
- <
- T
- >
- Success
- (
- T
- value
- )
- =>
- new
- (
- true
- ,
- value
- ,
- null
- ,
- null
- )
- ;
- public
- static
- Result
- <
- T
- >
- Failure
- (
- string
- error
- ,
- string
- ?
- code
- =
- null
- )
- =>
- new
- (
- false
- ,
- default
- ,
- error
- ,
- code
- )
- ;
- public
- Result
- <
- TNew
- >
- Map
- <
- TNew
- >
- (
- Func
- <
- T
- ,
- TNew
- >
- mapper
- )
- =>
- IsSuccess
- ?
- Result
- <
- TNew
- >
- .
- Success
- (
- mapper
- (
- Value
- !
- )
- )
- :
- Result
- <
- TNew
- >
- .
- Failure
- (
- Error
- !
- ,
- ErrorCode
- )
- ;
- public
- async
- Task
- <
- Result
- <
- TNew
- >
- >
- MapAsync
- <
- TNew
- >
- (
- Func
- <
- T
- ,
- Task
- <
- TNew
- >
- >
- mapper
- )
- =>
- IsSuccess
- ?
- Result
- <
- TNew
- >
- .
- Success
- (
- await
- mapper
- (
- Value
- !
- )
- )
- :
- Result
- <
- TNew
- >
- .
- Failure
- (
- Error
- !
- ,
- ErrorCode
- )
- ;
- }
- // Usage in service
- public
- async
- Task
- <
- Result
- <
- Order
- >
- >
- CreateOrderAsync
- (
- CreateOrderRequest
- request
- ,
- CancellationToken
- ct
- )
- {
- // Validation
- var
- validation
- =
- await
- _validator
- .
- ValidateAsync
- (
- request
- ,
- ct
- )
- ;
- if
- (
- !
- validation
- .
- IsValid
- )
- return
- Result
- <
- Order
- >
- .
- Failure
- (
- validation
- .
- Errors
- .
- First
- (
- )
- .
- ErrorMessage
- ,
- "VALIDATION_ERROR"
- )
- ;
- // Business rule check
- var
- stock
- =
- await
- _stockService
- .
- CheckAsync
- (
- request
- .
- ProductId
- ,
- request
- .
- Quantity
- ,
- ct
- )
- ;
- if
- (
- !
- stock
- .
- IsAvailable
- )
- return
- Result
- <
- Order
- >
- .
- Failure
- (
- $"Insufficient stock:
- {
- stock
- .
- Available
- }
- available,
- {
- request
- .
- Quantity
- }
- requested"
- ,
- "INSUFFICIENT_STOCK"
- )
- ;
- // Create order
- var
- order
- =
- await
- _repository
- .
- CreateAsync
- (
- request
- .
- ToEntity
- (
- )
- ,
- ct
- )
- ;
- return
- Result
- <
- Order
- >
- .
- Success
- (
- order
- )
- ;
- }
- // Usage in controller/endpoint
- app
- .
- MapPost
- (
- "/orders"
- ,
- async
- (
- CreateOrderRequest
- request
- ,
- IOrderService
- orderService
- ,
- CancellationToken
- ct
- )
- =>
- {
- var
- result
- =
- await
- orderService
- .
- CreateOrderAsync
- (
- request
- ,
- ct
- )
- ;
- return
- result
- .
- IsSuccess
- ?
- Results
- .
- Created
- (
- $"/orders/
- {
- result
- .
- Value
- !
- .
- Id
- }
- "
- ,
- result
- .
- Value
- )
- :
- Results
- .
- BadRequest
- (
- new
- {
- error
- =
- result
- .
- Error
- ,
- code
- =
- result
- .
- ErrorCode
- }
- )
- ;
- }
- )
- ;
- Data Access Patterns
- Entity Framework Core
- // DbContext configuration
- public
- class
- AppDbContext
- :
- DbContext
- {
- public
- DbSet
- <
- Product
- >
- Products
- =>
- Set
- <
- Product
- >
- (
- )
- ;
- public
- DbSet
- <
- Order
- >
- Orders
- =>
- Set
- <
- Order
- >
- (
- )
- ;
- protected
- override
- void
- OnModelCreating
- (
- ModelBuilder
- modelBuilder
- )
- {
- // Apply all configurations from assembly
- modelBuilder
- .
- ApplyConfigurationsFromAssembly
- (
- typeof
- (
- AppDbContext
- )
- .
- Assembly
- )
- ;
- // Global query filters
- modelBuilder
- .
- Entity
- <
- Product
- >
- (
- )
- .
- HasQueryFilter
- (
- p
- =>
- !
- p
- .
- IsDeleted
- )
- ;
- }
- }
- // Entity configuration
- public
- class
- ProductConfiguration
- :
- IEntityTypeConfiguration
- <
- Product
- >
- {
- public
- void
- Configure
- (
- EntityTypeBuilder
- <
- Product
- >
- builder
- )
- {
- builder
- .
- ToTable
- (
- "Products"
- )
- ;
- builder
- .
- HasKey
- (
- p
- =>
- p
- .
- Id
- )
- ;
- builder
- .
- Property
- (
- p
- =>
- p
- .
- Id
- )
- .
- HasMaxLength
- (
- 40
- )
- ;
- builder
- .
- Property
- (
- p
- =>
- p
- .
- Name
- )
- .
- HasMaxLength
- (
- 200
- )
- .
- IsRequired
- (
- )
- ;
- builder
- .
- Property
- (
- p
- =>
- p
- .
- Price
- )
- .
- HasPrecision
- (
- 18
- ,
- 2
- )
- ;
- builder
- .
- HasIndex
- (
- p
- =>
- p
- .
- Sku
- )
- .
- IsUnique
- (
- )
- ;
- builder
- .
- HasIndex
- (
- p
- =>
- new
- {
- p
- .
- CategoryId
- ,
- p
- .
- Name
- }
- )
- ;
- builder
- .
- HasMany
- (
- p
- =>
- p
- .
- OrderItems
- )
- .
- WithOne
- (
- oi
- =>
- oi
- .
- Product
- )
- .
- HasForeignKey
- (
- oi
- =>
- oi
- .
- ProductId
- )
- ;
- }
- }
- // Repository with EF Core
- public
- class
- ProductRepository
- :
- IProductRepository
- {
- private
- readonly
- AppDbContext
- _context
- ;
- public
- async
- Task
- <
- Product
- ?
- >
- GetByIdAsync
- (
- string
- id
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- return
- await
- _context
- .
- Products
- .
- AsNoTracking
- (
- )
- .
- FirstOrDefaultAsync
- (
- p
- =>
- p
- .
- Id
- ==
- id
- ,
- ct
- )
- ;
- }
- public
- async
- Task
- <
- IReadOnlyList
- <
- Product
- >
- >
- SearchAsync
- (
- ProductSearchCriteria
- criteria
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- query
- =
- _context
- .
- Products
- .
- AsNoTracking
- (
- )
- ;
- if
- (
- !
- string
- .
- IsNullOrWhiteSpace
- (
- criteria
- .
- SearchTerm
- )
- )
- query
- =
- query
- .
- Where
- (
- p
- =>
- EF
- .
- Functions
- .
- Like
- (
- p
- .
- Name
- ,
- $"%
- {
- criteria
- .
- SearchTerm
- }
- %"
- )
- )
- ;
- if
- (
- criteria
- .
- CategoryId
- .
- HasValue
- )
- query
- =
- query
- .
- Where
- (
- p
- =>
- p
- .
- CategoryId
- ==
- criteria
- .
- CategoryId
- )
- ;
- if
- (
- criteria
- .
- MinPrice
- .
- HasValue
- )
- query
- =
- query
- .
- Where
- (
- p
- =>
- p
- .
- Price
- >=
- criteria
- .
- MinPrice
- )
- ;
- if
- (
- criteria
- .
- MaxPrice
- .
- HasValue
- )
- query
- =
- query
- .
- Where
- (
- p
- =>
- p
- .
- Price
- <=
- criteria
- .
- MaxPrice
- )
- ;
- return
- await
- query
- .
- OrderBy
- (
- p
- =>
- p
- .
- Name
- )
- .
- Skip
- (
- (
- criteria
- .
- Page
- -
- 1
- )
- *
- criteria
- .
- PageSize
- )
- .
- Take
- (
- criteria
- .
- PageSize
- )
- .
- ToListAsync
- (
- ct
- )
- ;
- }
- }
- Dapper for Performance
- public
- class
- DapperProductRepository
- :
- IProductRepository
- {
- private
- readonly
- IDbConnection
- _connection
- ;
- public
- async
- Task
- <
- Product
- ?
- >
- GetByIdAsync
- (
- string
- id
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- const
- string
- sql
- =
- ""
- "
- SELECT
- Id
- ,
- Name
- ,
- Sku
- ,
- Price
- ,
- CategoryId
- ,
- Stock
- ,
- CreatedAt
- FROM Products
- WHERE
- Id
- =
- @Id
- AND
- IsDeleted
- =
- 0
- ""
- "
- ;
- return
- await
- _connection
- .
- QueryFirstOrDefaultAsync
- <
- Product
- >
- (
- new
- CommandDefinition
- (
- sql
- ,
- new
- {
- Id
- =
- id
- }
- ,
- cancellationToken
- :
- ct
- )
- )
- ;
- }
- public
- async
- Task
- <
- IReadOnlyList
- <
- Product
- >
- >
- SearchAsync
- (
- ProductSearchCriteria
- criteria
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- sql
- =
- new
- StringBuilder
- (
- ""
- "
- SELECT
- Id
- ,
- Name
- ,
- Sku
- ,
- Price
- ,
- CategoryId
- ,
- Stock
- ,
- CreatedAt
- FROM Products
- WHERE
- IsDeleted
- =
- 0
- ""
- "
- )
- ;
- var
- parameters
- =
- new
- DynamicParameters
- (
- )
- ;
- if
- (
- !
- string
- .
- IsNullOrWhiteSpace
- (
- criteria
- .
- SearchTerm
- )
- )
- {
- sql
- .
- Append
- (
- " AND Name LIKE @SearchTerm"
- )
- ;
- parameters
- .
- Add
- (
- "SearchTerm"
- ,
- $"%
- {
- criteria
- .
- SearchTerm
- }
- %"
- )
- ;
- }
- if
- (
- criteria
- .
- CategoryId
- .
- HasValue
- )
- {
- sql
- .
- Append
- (
- " AND CategoryId = @CategoryId"
- )
- ;
- parameters
- .
- Add
- (
- "CategoryId"
- ,
- criteria
- .
- CategoryId
- )
- ;
- }
- if
- (
- criteria
- .
- MinPrice
- .
- HasValue
- )
- {
- sql
- .
- Append
- (
- " AND Price >= @MinPrice"
- )
- ;
- parameters
- .
- Add
- (
- "MinPrice"
- ,
- criteria
- .
- MinPrice
- )
- ;
- }
- if
- (
- criteria
- .
- MaxPrice
- .
- HasValue
- )
- {
- sql
- .
- Append
- (
- " AND Price <= @MaxPrice"
- )
- ;
- parameters
- .
- Add
- (
- "MaxPrice"
- ,
- criteria
- .
- MaxPrice
- )
- ;
- }
- sql
- .
- Append
- (
- " ORDER BY Name OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY"
- )
- ;
- parameters
- .
- Add
- (
- "Offset"
- ,
- (
- criteria
- .
- Page
- -
- 1
- )
- *
- criteria
- .
- PageSize
- )
- ;
- parameters
- .
- Add
- (
- "PageSize"
- ,
- criteria
- .
- PageSize
- )
- ;
- var
- results
- =
- await
- _connection
- .
- QueryAsync
- <
- Product
- >
- (
- new
- CommandDefinition
- (
- sql
- .
- ToString
- (
- )
- ,
- parameters
- ,
- cancellationToken
- :
- ct
- )
- )
- ;
- return
- results
- .
- ToList
- (
- )
- ;
- }
- // Multi-mapping for related data
- public
- async
- Task
- <
- Order
- ?
- >
- GetOrderWithItemsAsync
- (
- int
- orderId
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- const
- string
- sql
- =
- ""
- "
- SELECT o
- .
- *
- ,
- oi
- .
- *
- ,
- p
- .
- *
- FROM Orders o
- LEFT JOIN OrderItems oi ON o
- .
- Id
- =
- oi
- .
- OrderId
- LEFT JOIN Products p ON oi
- .
- ProductId
- =
- p
- .
- Id
- WHERE o
- .
- Id
- =
- @OrderId
- ""
- "
- ;
- var
- orderDictionary
- =
- new
- Dictionary
- <
- int
- ,
- Order
- >
- (
- )
- ;
- await
- _connection
- .
- QueryAsync
- <
- Order
- ,
- OrderItem
- ,
- Product
- ,
- Order
- >
- (
- new
- CommandDefinition
- (
- sql
- ,
- new
- {
- OrderId
- =
- orderId
- }
- ,
- cancellationToken
- :
- ct
- )
- ,
- (
- order
- ,
- item
- ,
- product
- )
- =>
- {
- if
- (
- !
- orderDictionary
- .
- TryGetValue
- (
- order
- .
- Id
- ,
- out
- var
- existingOrder
- )
- )
- {
- existingOrder
- =
- order
- ;
- existingOrder
- .
- Items
- =
- new
- List
- <
- OrderItem
- >
- (
- )
- ;
- orderDictionary
- .
- Add
- (
- order
- .
- Id
- ,
- existingOrder
- )
- ;
- }
- if
- (
- item
- !=
- null
- )
- {
- item
- .
- Product
- =
- product
- ;
- existingOrder
- .
- Items
- .
- Add
- (
- item
- )
- ;
- }
- return
- existingOrder
- ;
- }
- ,
- splitOn
- :
- "Id,Id"
- )
- ;
- return
- orderDictionary
- .
- Values
- .
- FirstOrDefault
- (
- )
- ;
- }
- }
- Caching Patterns
- Multi-Level Cache with Redis
- public
- class
- CachedProductService
- :
- IProductService
- {
- private
- readonly
- IProductRepository
- _repository
- ;
- private
- readonly
- IMemoryCache
- _memoryCache
- ;
- private
- readonly
- IDistributedCache
- _distributedCache
- ;
- private
- readonly
- ILogger
- <
- CachedProductService
- >
- _logger
- ;
- private
- static
- readonly
- TimeSpan
- MemoryCacheDuration
- =
- TimeSpan
- .
- FromMinutes
- (
- 1
- )
- ;
- private
- static
- readonly
- TimeSpan
- DistributedCacheDuration
- =
- TimeSpan
- .
- FromMinutes
- (
- 15
- )
- ;
- public
- async
- Task
- <
- Product
- ?
- >
- GetByIdAsync
- (
- string
- id
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- cacheKey
- =
- $"product:
- {
- id
- }
- "
- ;
- // L1: Memory cache (in-process, fastest)
- if
- (
- _memoryCache
- .
- TryGetValue
- (
- cacheKey
- ,
- out
- Product
- ?
- cached
- )
- )
- {
- _logger
- .
- LogDebug
- (
- "L1 cache hit for {CacheKey}"
- ,
- cacheKey
- )
- ;
- return
- cached
- ;
- }
- // L2: Distributed cache (Redis)
- var
- distributed
- =
- await
- _distributedCache
- .
- GetStringAsync
- (
- cacheKey
- ,
- ct
- )
- ;
- if
- (
- distributed
- !=
- null
- )
- {
- _logger
- .
- LogDebug
- (
- "L2 cache hit for {CacheKey}"
- ,
- cacheKey
- )
- ;
- var
- product
- =
- JsonSerializer
- .
- Deserialize
- <
- Product
- >
- (
- distributed
- )
- ;
- // Populate L1
- _memoryCache
- .
- Set
- (
- cacheKey
- ,
- product
- ,
- MemoryCacheDuration
- )
- ;
- return
- product
- ;
- }
- // L3: Database
- _logger
- .
- LogDebug
- (
- "Cache miss for {CacheKey}, fetching from database"
- ,
- cacheKey
- )
- ;
- var
- fromDb
- =
- await
- _repository
- .
- GetByIdAsync
- (
- id
- ,
- ct
- )
- ;
- if
- (
- fromDb
- !=
- null
- )
- {
- var
- serialized
- =
- JsonSerializer
- .
- Serialize
- (
- fromDb
- )
- ;
- // Populate both caches
- await
- _distributedCache
- .
- SetStringAsync
- (
- cacheKey
- ,
- serialized
- ,
- new
- DistributedCacheEntryOptions
- {
- AbsoluteExpirationRelativeToNow
- =
- DistributedCacheDuration
- }
- ,
- ct
- )
- ;
- _memoryCache
- .
- Set
- (
- cacheKey
- ,
- fromDb
- ,
- MemoryCacheDuration
- )
- ;
- }
- return
- fromDb
- ;
- }
- public
- async
- Task
- InvalidateAsync
- (
- string
- id
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- cacheKey
- =
- $"product:
- {
- id
- }
- "
- ;
- _memoryCache
- .
- Remove
- (
- cacheKey
- )
- ;
- await
- _distributedCache
- .
- RemoveAsync
- (
- cacheKey
- ,
- ct
- )
- ;
- _logger
- .
- LogInformation
- (
- "Invalidated cache for {CacheKey}"
- ,
- cacheKey
- )
- ;
- }
- }
- // Stale-while-revalidate pattern
- public
- class
- StaleWhileRevalidateCache
- <
- T
- >
- {
- private
- readonly
- IDistributedCache
- _cache
- ;
- private
- readonly
- TimeSpan
- _freshDuration
- ;
- private
- readonly
- TimeSpan
- _staleDuration
- ;
- public
- async
- Task
- <
- T
- ?
- >
- GetOrCreateAsync
- (
- string
- key
- ,
- Func
- <
- CancellationToken
- ,
- Task
- <
- T
- >
- >
- factory
- ,
- CancellationToken
- ct
- =
- default
- )
- {
- var
- cached
- =
- await
- _cache
- .
- GetStringAsync
- (
- key
- ,
- ct
- )
- ;
- if
- (
- cached
- !=
- null
- )
- {
- var
- entry
- =
- JsonSerializer
- .
- Deserialize
- <
- CacheEntry
- <
- T
- >
- >
- (
- cached
- )
- !
- ;
- if
- (
- entry
- .
- IsStale
- &&
- !
- entry
- .
- IsExpired
- )
- {
- // Return stale data immediately, refresh in background
- _
- =
- Task
- .
- Run
- (
- async
- (
- )
- =>
- {
- var
- fresh
- =
- await
- factory
- (
- CancellationToken
- .
- None
- )
- ;
- await
- SetAsync
- (
- key
- ,
- fresh
- ,
- CancellationToken
- .
- None
- )
- ;
- }
- )
- ;
- }
- if
- (
- !
- entry
- .
- IsExpired
- )
- return
- entry
- .
- Value
- ;
- }
- // Cache miss or expired
- var
- value
- =
- await
- factory
- (
- ct
- )
- ;
- await
- SetAsync
- (
- key
- ,
- value
- ,
- ct
- )
- ;
- return
- value
- ;
- }
- private
- record
- CacheEntry
- <
- TValue
- >
- (
- TValue
- Value
- ,
- DateTime
- CreatedAt
- )
- {
- public
- bool
- IsStale
- =>
- DateTime
- .
- UtcNow
- -
- CreatedAt
- >
- _freshDuration
- ;
- public
- bool
- IsExpired
- =>
- DateTime
- .
- UtcNow
- -
- CreatedAt
- >
- _staleDuration
- ;
- }
- }
- Testing Patterns
- Unit Tests with xUnit and Moq
- public
- class
- OrderServiceTests
- {
- private
- readonly
- Mock
- <
- IOrderRepository
- >
- _mockRepository
- ;
- private
- readonly
- Mock
- <
- IStockService
- >
- _mockStockService
- ;
- private
- readonly
- Mock
- <
- IValidator
- <
- CreateOrderRequest
- >
- >
- _mockValidator
- ;
- private
- readonly
- OrderService
- _sut
- ;
- // System Under Test
- public
- OrderServiceTests
- (
- )
- {
- _mockRepository
- =
- new
- Mock
- <
- IOrderRepository
- >
- (
- )
- ;
- _mockStockService
- =
- new
- Mock
- <
- IStockService
- >
- (
- )
- ;
- _mockValidator
- =
- new
- Mock
- <
- IValidator
- <
- CreateOrderRequest
- >
- >
- (
- )
- ;
- // Default: validation passes
- _mockValidator
- .
- Setup
- (
- v
- =>
- v
- .
- ValidateAsync
- (
- It
- .
- IsAny
- <
- CreateOrderRequest
- >
- (
- )
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- )
- .
- ReturnsAsync
- (
- new
- ValidationResult
- (
- )
- )
- ;
- _sut
- =
- new
- OrderService
- (
- _mockRepository
- .
- Object
- ,
- _mockStockService
- .
- Object
- ,
- _mockValidator
- .
- Object
- )
- ;
- }
- [
- Fact
- ]
- public
- async
- Task
- CreateOrderAsync_WithValidRequest_ReturnsSuccess
- (
- )
- {
- // Arrange
- var
- request
- =
- new
- CreateOrderRequest
- {
- ProductId
- =
- "PROD-001"
- ,
- Quantity
- =
- 5
- ,
- CustomerOrderCode
- =
- "ORD-2024-001"
- }
- ;
- _mockStockService
- .
- Setup
- (
- s
- =>
- s
- .
- CheckAsync
- (
- "PROD-001"
- ,
- 5
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- )
- .
- ReturnsAsync
- (
- new
- StockResult
- {
- IsAvailable
- =
- true
- ,
- Available
- =
- 10
- }
- )
- ;
- _mockRepository
- .
- Setup
- (
- r
- =>
- r
- .
- CreateAsync
- (
- It
- .
- IsAny
- <
- Order
- >
- (
- )
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- )
- .
- ReturnsAsync
- (
- new
- Order
- {
- Id
- =
- 1
- ,
- CustomerOrderCode
- =
- "ORD-2024-001"
- }
- )
- ;
- // Act
- var
- result
- =
- await
- _sut
- .
- CreateOrderAsync
- (
- request
- )
- ;
- // Assert
- Assert
- .
- True
- (
- result
- .
- IsSuccess
- )
- ;
- Assert
- .
- NotNull
- (
- result
- .
- Value
- )
- ;
- Assert
- .
- Equal
- (
- 1
- ,
- result
- .
- Value
- .
- Id
- )
- ;
- _mockRepository
- .
- Verify
- (
- r
- =>
- r
- .
- CreateAsync
- (
- It
- .
- Is
- <
- Order
- >
- (
- o
- =>
- o
- .
- CustomerOrderCode
- ==
- "ORD-2024-001"
- )
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- ,
- Times
- .
- Once
- )
- ;
- }
- [
- Fact
- ]
- public
- async
- Task
- CreateOrderAsync_WithInsufficientStock_ReturnsFailure
- (
- )
- {
- // Arrange
- var
- request
- =
- new
- CreateOrderRequest
- {
- ProductId
- =
- "PROD-001"
- ,
- Quantity
- =
- 100
- }
- ;
- _mockStockService
- .
- Setup
- (
- s
- =>
- s
- .
- CheckAsync
- (
- It
- .
- IsAny
- <
- string
- >
- (
- )
- ,
- It
- .
- IsAny
- <
- int
- >
- (
- )
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- )
- .
- ReturnsAsync
- (
- new
- StockResult
- {
- IsAvailable
- =
- false
- ,
- Available
- =
- 5
- }
- )
- ;
- // Act
- var
- result
- =
- await
- _sut
- .
- CreateOrderAsync
- (
- request
- )
- ;
- // Assert
- Assert
- .
- False
- (
- result
- .
- IsSuccess
- )
- ;
- Assert
- .
- Equal
- (
- "INSUFFICIENT_STOCK"
- ,
- result
- .
- ErrorCode
- )
- ;
- Assert
- .
- Contains
- (
- "5 available"
- ,
- result
- .
- Error
- )
- ;
- _mockRepository
- .
- Verify
- (
- r
- =>
- r
- .
- CreateAsync
- (
- It
- .
- IsAny
- <
- Order
- >
- (
- )
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- ,
- Times
- .
- Never
- )
- ;
- }
- [
- Theory
- ]
- [
- InlineData
- (
- 0
- )
- ]
- [
- InlineData
- (
- -
- 1
- )
- ]
- [
- InlineData
- (
- -
- 100
- )
- ]
- public
- async
- Task
- CreateOrderAsync_WithInvalidQuantity_ReturnsValidationError
- (
- int
- quantity
- )
- {
- // Arrange
- var
- request
- =
- new
- CreateOrderRequest
- {
- ProductId
- =
- "PROD-001"
- ,
- Quantity
- =
- quantity
- }
- ;
- _mockValidator
- .
- Setup
- (
- v
- =>
- v
- .
- ValidateAsync
- (
- request
- ,
- It
- .
- IsAny
- <
- CancellationToken
- >
- (
- )
- )
- )
- .
- ReturnsAsync
- (
- new
- ValidationResult
- (
- new
- [
- ]
- {
- new
- ValidationFailure
- (
- "Quantity"
- ,
- "Quantity must be greater than 0"
- )
- }
- )
- )
- ;
- // Act
- var
- result
- =
- await
- _sut
- .
- CreateOrderAsync
- (
- request
- )
- ;
- // Assert
- Assert
- .
- False
- (
- result
- .
- IsSuccess
- )
- ;
- Assert
- .
- Equal
- (
- "VALIDATION_ERROR"
- ,
- result
- .
- ErrorCode
- )
- ;
- }
- }
- Integration Tests with WebApplicationFactory
- public
- class
- ProductsApiTests
- :
- IClassFixture
- <
- WebApplicationFactory
- <
- Program
- >
- >
- {
- private
- readonly
- WebApplicationFactory
- <
- Program
- >
- _factory
- ;
- private
- readonly
- HttpClient
- _client
- ;
- public
- ProductsApiTests
- (
- WebApplicationFactory
- <
- Program
- >
- factory
- )
- {
- _factory
- =
- factory
- .
- WithWebHostBuilder
- (
- builder
- =>
- {
- builder
- .
- ConfigureServices
- (
- services
- =>
- {
- // Replace real database with in-memory
- services
- .
- RemoveAll
- <
- DbContextOptions
- <
- AppDbContext
- >
- >
- (
- )
- ;
- services
- .
- AddDbContext
- <
- AppDbContext
- >
- (
- options
- =>
- options
- .
- UseInMemoryDatabase
- (
- "TestDb"
- )
- )
- ;
- // Replace Redis with memory cache
- services
- .
- RemoveAll
- <
- IDistributedCache
- >
- (
- )
- ;
- services
- .
- AddDistributedMemoryCache
- (
- )
- ;
- }
- )
- ;
- }
- )
- ;
- _client
- =
- _factory
- .
- CreateClient
- (
- )
- ;
- }
- [
- Fact
- ]
- public
- async
- Task
- GetProduct_WithValidId_ReturnsProduct
- (
- )
- {
- // Arrange
- using
- var
- scope
- =
- _factory
- .
- Services
- .
- CreateScope
- (
- )
- ;
- var
- context
- =
- scope
- .
- ServiceProvider
- .
- GetRequiredService
- <
- AppDbContext
- >
- (
- )
- ;
- context
- .
- Products
- .
- Add
- (
- new
- Product
- {
- Id
- =
- "TEST-001"
- ,
- Name
- =
- "Test Product"
- ,
- Price
- =
- 99.99m
- }
- )
- ;
- await
- context
- .
- SaveChangesAsync
- (
- )
- ;
- // Act
- var
- response
- =
- await
- _client
- .
- GetAsync
- (
- "/api/products/TEST-001"
- )
- ;
- // Assert
- response
- .
- EnsureSuccessStatusCode
- (
- )
- ;
- var
- product
- =
- await
- response
- .
- Content
- .
- ReadFromJsonAsync
- <
- Product
- >
- (
- )
- ;
- Assert
- .
- Equal
- (
- "Test Product"
- ,
- product
- !
- .
- Name
- )
- ;
- }
- [
- Fact
- ]
- public
- async
- Task
- GetProduct_WithInvalidId_Returns404
- (
- )
- {
- // Act
- var
- response
- =
- await
- _client
- .
- GetAsync
- (
- "/api/products/NONEXISTENT"
- )
- ;
- // Assert
- Assert
- .
- Equal
- (
- HttpStatusCode
- .
- NotFound
- ,
- response
- .
- StatusCode
- )
- ;
- }
- }
- Best Practices
- DO
- Use async/await
- all the way through the call stack
- Inject dependencies
- through constructor injection
- Use IOptions
- for typed configuration
- Return Result types
- instead of throwing exceptions for business logic
- Use CancellationToken
- in all async methods
- Prefer Dapper
- for read-heavy, performance-critical queries
- Use EF Core
- for complex domain models with change tracking
- Cache aggressively
- with proper invalidation strategies
- Write unit tests
- for business logic, integration tests for APIs
- Use record types
- for DTOs and immutable data
- DON'T
- Don't block on async
- with
- .Result
- or
- .Wait()
- Don't use async void
- except for event handlers
- Don't catch generic Exception
- without re-throwing or logging
- Don't hardcode
- configuration values
- Don't expose EF entities
- directly in APIs (use DTOs)
- Don't forget
- AsNoTracking()
- for read-only queries
- Don't ignore
- CancellationToken parameters
- Don't create
- new HttpClient()
- manually (use IHttpClientFactory)
- Don't mix
- sync and async code unnecessarily
- Don't skip
- validation at API boundaries
- Common Pitfalls
- N+1 Queries
-
- Use
- .Include()
- or explicit joins
- Memory Leaks
-
- Dispose IDisposable resources, use
- using
- Deadlocks
-
- Don't mix sync and async, use ConfigureAwait(false) in libraries
- Over-fetching
-
- Select only needed columns, use projections
- Missing Indexes
-
- Check query plans, add indexes for common filters
- Timeout Issues
-
- Configure appropriate timeouts for HTTP clients
- Cache Stampede
- Use distributed locks for cache population
← 返回排行榜