dotnet-patterns

dotnet-patterns

熱門

慣用的 C# 與 .NET 模式、慣例、相依性注入、async/await,以及建構穩固、可維護 .NET 應用程式的最佳做法。

23萬星標
3.5萬分支
更新於 2026/7/20
SKILL.md
readonlyread-only
name
dotnet-patterns
description

慣用的 C# 與 .NET 模式、慣例、相依性注入、async/await,以及建構穩固、可維護 .NET 應用程式的最佳做法。

.NET 開發模式

用於建構穩固、高效能且可維護應用程式的慣用 C# 與 .NET 模式。

何時啟用

  • 撰寫新的 C# 程式碼
  • 審查 C# 程式碼
  • 重構現有 .NET 應用程式
  • 使用 ASP.NET Core 設計服務架構

核心原則

1. 偏好不可變性

對資料模型使用 record 和 init-only 屬性。可變性應為明確且有正當理由的選擇。

// 良好:不可變的實值物件
public sealed record Money(decimal Amount, string Currency);

// 良好:使用 init 設定子的不可變 DTO
public sealed class CreateOrderRequest
{
    public required string CustomerId { get; init; }
    public required IReadOnlyList<OrderItem> Items { get; init; }
}

// 不良:具有公用設定子的可變模型
public class Order
{
    public string CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

2. 明確勝於隱含

清楚表達可為 null 性、存取修飾詞與意圖。

// 良好:明確的存取修飾詞與可為 null 性
public sealed class UserService
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repository, ILogger<UserService> logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await _repository.FindByIdAsync(id, cancellationToken);
    }
}

3. 依賴抽象

對服務邊界使用介面。透過 DI 容器註冊。

// 良好:基於介面的相依性
public interface IOrderRepository
{
    Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
    Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
    Task AddAsync(Order order, CancellationToken cancellationToken);
}

// 註冊
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

Async/Await 模式

正確的非同步用法

// 良好:一路非同步,並使用 CancellationToken
public async Task<OrderSummary> GetOrderSummaryAsync(
    Guid orderId,
    CancellationToken cancellationToken)
{
    var order = await _repository.FindByIdAsync(orderId, cancellationToken)
        ?? throw new NotFoundException($"Order {orderId} not found");

    var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);

    return new OrderSummary(order, customer);
}

// 不良:在非同步上阻塞
public OrderSummary GetOrderSummary(Guid orderId)
{
    var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // 死結風險
    return new OrderSummary(order);
}

平行非同步作業

// 良好:並行獨立作業
public async Task<DashboardData> LoadDashboardAsync(CancellationToken cancellationToken)
{
    var ordersTask = _orderService.GetRecentAsync(cancellationToken);
    var metricsTask = _metricsService.GetCurrentAsync(cancellationToken);
    var alertsTask = _alertService.GetActiveAsync(cancellationToken);

    await Task.WhenAll(ordersTask, metricsTask, alertsTask);

    return new DashboardData(
        Orders: await ordersTask,
        Metrics: await metricsTask,
        Alerts: await alertsTask);
}

Options 模式

將設定區段繫結至強型別物件。

public sealed class SmtpOptions
{
    public const string SectionName = "Smtp";

    public required string Host { get; init; }
    public required int Port { get; init; }
    public required string Username { get; init; }
    public bool UseSsl { get; init; } = true;
}

// 註冊
builder.Services.Configure<SmtpOptions>(
    builder.Configuration.GetSection(SmtpOptions.SectionName));

// 透過注入使用
public class EmailService(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;
}

Result 模式

針對預期的失敗,傳回明確的成功/失敗,而非擲回例外。

public sealed record Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(T value) { IsSuccess = true; Value = value; }
    private Result(string error) { IsSuccess = false; Error = error; }

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(string error) => new(error);
}

// 使用方式
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
{
    if (request.Items.Count == 0)
        return Result<Order>.Failure("訂單必須包含至少一個項目");

    var order = Order.Create(request);
    await _repository.AddAsync(order, CancellationToken.None);
    return Result<Order>.Success(order);
}

搭配 EF Core 的 Repository 模式

public sealed class SqlOrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;

    public SqlOrderRepository(AppDbContext db) => _db = db;

    public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
    {
        return await _db.Orders
            .Include(o => o.Items)
            .AsNoTracking()
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task<IReadOnlyList<Order>> FindByCustomerAsync(
        string customerId,
        CancellationToken cancellationToken)
    {
        return await _db.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }

    public async Task AddAsync(Order order, CancellationToken cancellationToken)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(cancellationToken);
    }
}

中介軟體與管線

// 自訂中介軟體
public sealed class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            _logger.LogInformation(
                "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
                context.Request.Method,
                context.Request.Path,
                stopwatch.ElapsedMilliseconds,
                context.Response.StatusCode);
        }
    }
}

Minimal API 模式

// 使用路由群組組織
var orders = app.MapGroup("/api/orders")
    .RequireAuthorization()
    .WithTags("Orders");

orders.MapGet("/{id:guid}", async (
    Guid id,
    IOrderRepository repository,
    CancellationToken cancellationToken) =>
{
    var order = await repository.FindByIdAsync(id, cancellationToken);
    return order is not null
        ? TypedResults.Ok(order)
        : TypedResults.NotFound();
});

orders.MapPost("/", async (
    CreateOrderRequest request,
    IOrderService service,
    CancellationToken cancellationToken) =>
{
    var result = await service.PlaceOrderAsync(request, cancellationToken);
    return result.IsSuccess
        ? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value)
        : TypedResults.BadRequest(result.Error);
});

Guard Clauses

// 良好:使用明確驗證的早期回傳
public async Task<ProcessResult> ProcessPaymentAsync(
    PaymentRequest request,
    CancellationToken cancellationToken)
{
    ArgumentNullException.ThrowIfNull(request);

    if (request.Amount <= 0)
        throw new ArgumentOutOfRangeException(nameof(request.Amount), "金額必須為正數");

    if (string.IsNullOrWhiteSpace(request.Currency))
        throw new ArgumentException"幣別為必填", nameof(request.Currency));

    // 快樂路徑在此繼續,無需巢狀
    var gateway = _gatewayFactory.Create(request.Currency);
    return await gateway.ChargeAsync(request, cancellationToken);
}

應避免的反模式

反模式 修正方式
async void 方法 回傳 Task(事件處理常式除外)
.Result.Wait() 使用 await
catch (Exception) { } 處理或重新擲回並附帶上下文
在建構函式中使用 new Service() 使用建構函式注入
public 欄位 使用具有適當存取子的屬性
在商業邏輯中使用 dynamic 使用泛型或明確型別
可變的 static 狀態 使用 DI 範圍或 ConcurrentDictionary
在迴圈中使用 string.Format 使用 StringBuilder 或字串插值處理常式