name: dotnet-backend-patterns description: 掌握C#/.NET后端开发模式,用于构建稳健的API、MCP服务器和企业应用。涵盖异步/等待、依赖注入、Entity Framework Core、Dapper、配置、缓存和使用xUnit进行测试。在开发.NET后端、审查C#代码或设计API架构时使用。
.NET后端开发模式
掌握C#/.NET模式,用于构建生产级API、MCP服务器和企业后端,采用现代最佳实践(2024/2025)。
何时使用此技能
- 开发新的.NET Web API或MCP服务器
- 审查C#代码的质量和性能
- 设计带有依赖注入的服务架构
- 使用Redis实现缓存策略
- 编写单元和集成测试
- 使用EF Core或Dapper优化数据库访问
- 使用IOptions模式配置应用程序
- 处理错误并实现弹性模式
核心概念
1. 项目结构(清洁架构)
src/
├── Domain/ # 核心业务逻辑(无依赖)
│ ├── Entities/
│ ├── Interfaces/
│ ├── Exceptions/
│ └── ValueObjects/
├── Application/ # 用例、DTO、验证
│ ├── Services/
│ ├── DTOs/
│ ├── Validators/
│ └── Interfaces/
├── Infrastructure/ # 外部实现
│ ├── Data/ # EF Core、Dapper仓库
│ ├── Caching/ # Redis、内存缓存
│ ├── External/ # HTTP客户端、第三方API
│ └── DependencyInjection/ # 服务注册
└── Api/ # 入口点
├── Controllers/ # 或MinimalAPI端点
├── Middleware/
├── Filters/
└── Program.cs
2. 依赖注入模式
// 按生命周期服务注册
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services,
IConfiguration configuration)
{
// Scoped:每个HTTP请求一个实例
services.AddScoped<IProductService, ProductService>();
services.AddScoped<IOrderService, OrderService>();
// Singleton:应用生命周期一个实例
services.AddSingleton<ICacheService, RedisCacheService>();
services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect(configuration["Redis:Connection"]!));
// Transient:每次新实例
services.AddTransient<IValidator<CreateOrderRequest>, CreateOrderValidator>();
// 配置的选项模式
services.Configure<CatalogOptions>(configuration.GetSection("Catalog"));
services.Configure<RedisOptions>(configuration.GetSection("Redis"));
// 工厂模式用于条件创建
services.AddScoped<IPriceCalculator>(sp =>
{
var options = sp.GetRequiredService<IOptions<PricingOptions>>().Value;
return options.UseNewEngine
? sp.GetRequiredService<NewPriceCalculator>()
: sp.GetRequiredService<LegacyPriceCalculator>();
});
// 键控服务(.NET 8+)
services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
return services;
}
}
// 使用键控服务
public class CheckoutService
{
public CheckoutService(
[FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor)
{
_processor = stripeProcessor;
}
}
3. 异步/等待模式
// ✅ 正确:一直向下异步
public async Task<Product> GetProductAsync(string id, CancellationToken ct = default)
{
return await _repository.GetByIdAsync(id, ct);
}
// ✅ 正确:使用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);
}
// ✅ 正确:库中使用ConfigureAwait
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);
}
// ✅ 正确:热路径中使用ValueTask和缓存
public ValueTask<Product?> GetCachedProductAsync(string id)
{
if (_cache.TryGetValue(id, out Product? product))
return ValueTask.FromResult(product);
return new ValueTask<Product?>(GetFromDatabaseAsync(id));
}
// ❌ 错误:阻塞异步(死锁风险)
var result = GetProductAsync(id).Result; // 永远不要这样做
var result2 = GetProductAsync(id).GetAwaiter().GetResult(); // 也不好
// ❌ 错误:async void(除了事件处理程序)
public async void ProcessOrder() { } // 异常会丢失
// ❌ 错误:对已异步的代码不必要地使用Task.Run
await Task.Run(async () => await GetDataAsync()); // 浪费线程
4. 使用IOptions进行配置
// 配置类
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
}
}
// 注册
services.Configure<CatalogOptions>(configuration.GetSection(CatalogOptions.SectionName));
services.Configure<RedisOptions>(configuration.GetSection(RedisOptions.SectionName));
// 使用IOptions(单例,启动时读取一次)
public class CatalogService
{
private readonly CatalogOptions _options;
public CatalogService(IOptions<CatalogOptions> options)
{
_options = options.Value;
}
}
// 使用IOptionsSnapshot(作用域,每个请求重新读取)
public class DynamicService
{
private readonly CatalogOptions _options;
public DynamicService(IOptionsSnapshot<CatalogOptions> options)
{
_options = options.Value; // 每个请求新值
}
}
// 使用IOptionsMonitor(单例,变化时通知)
public class MonitoredService
{
private CatalogOptions _options;
public MonitoredService(IOptionsMonitor<CatalogOptions> monitor)
{
_options = monitor.CurrentValue;
monitor.OnChange(newOptions => _options = newOptions);
}
}
5. 结果模式(避免使用异常进行流程控制)
// 泛型结果类型
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);
}
// 在服务中使用
public async Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
{
// 验证
var validation = await _validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return Result<Order>.Failure(
validation.Errors.First().ErrorMessage,
"VALIDATION_ERROR");
// 业务规则检查
var stock = await _stockService.CheckAsync(request.ProductId, request.Quantity, ct);
if (!stock.IsAvailable)
return Result<Order>.Failure(
$"库存不足:可用{stock.Available},请求{request.Quantity}",
"INSUFFICIENT_STOCK");
// 创建订单
var order = await _repository.CreateAsync(request.ToEntity(), ct);
return Result<Order>.Success(order);
}
// 在控制器/端点中使用
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 });
});
数据访问模式
Entity Framework Core
// DbContext配置
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 从程序集应用所有配置
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// 全局查询过滤器
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);
}
}
// 实体配置
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);
}
}
// 使用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用于性能
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();
}
// 多映射用于相关数据
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();
}
}
缓存模式
使用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:内存缓存(进程内,最快)
if (_memoryCache.TryGetValue(cacheKey, out Product? cached))
{
_logger.LogDebug("L1缓存命中 {CacheKey}", cacheKey);
return cached;
}
// L2:分布式缓存(Redis)
var distributed = await _distributedCache.GetStringAsync(cacheKey, ct);
if (distributed != null)
{
_logger.LogDebug("L2缓存命中 {CacheKey}", cacheKey);
var product = JsonSerializer.Deserialize<Product>(distributed);
// 填充L1
_memoryCache.Set(cacheKey, product, MemoryCacheDuration);
return product;
}
// L3:数据库
_logger.LogDebug("缓存未命中 {CacheKey},从数据库获取", cacheKey);
var fromDb = await _repository.GetByIdAsync(id, ct);
if (fromDb != null)
{
var serialized = JsonSerializer.Serialize(fromDb);
// 填充两个缓存
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("缓存失效 {CacheKey}", cacheKey);
}
}
// 陈旧但重新验证模式
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)
{
// 立即返回陈旧数据,后台刷新
_ = Task.Run(async () =>
{
var fresh = await factory(CancellationToken.None);
await SetAsync(key, fresh, CancellationToken.None);
});
}
if (!entry.IsExpired)
return entry.Value;
}
// 缓存未命中或过期
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;
}
}
测试模式
使用xUnit和Moq的单元测试
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _mockRepository;
private readonly Mock<IStockService> _mockStockService;
private readonly Mock<IValidator<CreateOrderRequest>> _mockValidator;
private readonly OrderService _sut; // 待测系统
public OrderServiceTests()
{
_mockRepository = new Mock<IOrderRepository>();
_mockStockService = new Mock<IStockService>();
_mockValidator = new Mock<IValidator<CreateOrderRequest>>();
// 默认:验证通过
_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()
{
// 准备
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" });
// 执行
var result = await _sut.CreateOrderAsync(request);
// 断言
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()
{
// 准备
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 });
// 执行
var result = await _sut.CreateOrderAsync(request);
// 断言
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)
{
// 准备
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")
}));
// 执行
var result = await _sut.CreateOrderAsync(request);
// 断言
Assert.False(result.IsSuccess);
Assert.Equal("VALIDATION_ERROR", result.ErrorCode);
}
}
使用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 =>
{
// 用内存数据库替换真实数据库
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
// 用内存缓存替换Redis
services.RemoveAll<IDistributedCache>();
services.AddDistributedMemoryCache();
});
});
_client = _factory.CreateClient();
}
[Fact]
public async Task GetProduct_WithValidId_ReturnsProduct()
{
// 准备
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Products.Add(new Product
{
Id = "TEST-001",
Name = "测试产品",
Price = 99.99m
});
await context.SaveChangesAsync();
// 执行
var response = await _client.GetAsync("/api/products/TEST-001");
// 断言
response.EnsureSuccessStatusCode();
var product = await response.Content.ReadFromJsonAsync<Product>();
Assert.Equal("测试产品", product!.Name);
}
[Fact]
public async Task GetProduct_WithInvalidId_Returns404()
{
// 执行
var response = await _client.GetAsync("/api/products/NONEXISTENT");
// 断言
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
最佳实践
应该做
- 使用async/await 贯穿整个调用栈
- 通过构造函数注入依赖
- 使用IOptions<T> 进行类型化配置
- 返回结果类型 而不是为业务逻辑抛出异常
- 在所有异步方法中使用CancellationToken
- 优先使用Dapper 用于读密集型、性能关键的查询
- 使用EF Core 用于具有变更跟踪的复杂领域模型
- 积极缓存 并采用适当的失效策略
- 编写单元测试 用于业务逻辑,集成测试用于API
- 使用记录类型 用于DTO和不可变数据
不应该做
- 不要阻塞异步 使用
.Result或.Wait() - 不要使用async void 除了事件处理程序
- 不要捕获通用Exception 而不重新抛出或记录
- 不要硬编码 配置值
- 不要在API中直接暴露EF实体 (使用DTO)
- 不要忘记 用于只读查询的
AsNoTracking() - 不要忽略 CancellationToken参数
- 不要手动创建
new HttpClient()(使用IHttpClientFactory) - 不要不必要地混合同步和异步代码
- 不要跳过 API边界的验证
常见陷阱
- N+1查询:使用
.Include()或显式连接 - 内存泄漏:释放IDisposable资源,使用
using - 死锁:不要混合同步和异步,库中使用ConfigureAwait(false)
- 过度获取:仅选择需要的列,使用投影
- 缺少索引:检查查询计划,为常见过滤器添加索引
- 超时问题:为HTTP客户端配置适当超时
- 缓存雪崩:使用分布式锁进行缓存填充
资源
- assets/service-template.cs:完整的服务实现模板
- assets/repository-template.cs:仓库模式实现
- references/ef-core-best-practices.md:EF Core优化指南
- references/dapper-patterns.md:高级Dapper使用模式