iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
佛心分享-SideProject30

30天的旅程!從學習C#到開發小專案系列 第 14

DAY 14 - 讓程式碼更乾淨:ASP.NET Core中的服務層(Service Layer)和依賴注入(DI)

  • 分享至 

  • xImage
  •  

哈囉大家好!
今天要來介紹ASP.NET Core中的服務層(Service Layer)以及依賴注入(DI)。

什麼是服務層(Service Layer)?

服務層是位於Controller和資料庫存取(EF Core Context)之間的中間層!

為什麼需要服務層?

服務層的功能主要是用來分離職責!
Controller應該只要負責處理HTTP請求,例如:驗證請求格式、呼叫服務)。
服務層則是負責處理業務邏輯(Business Logic),例如:資料庫操作、計算、驗證規則...等。
然後資料庫層(EF Core)專門負責資料的增刪查改
透過分層的方式,可以提高程式碼的可讀性和可擴展性,也讓程式碼更易於測試(獨立測試業務邏輯)。

認識依賴注入(Dependency injection)

  • 什麼是依賴
    當一個類別(例如:Controller)需要使用另一個類別(例如:Service)來完成它的任務時,就可以說前者依賴於後者(例如:某個Controller依賴某個Service)。
  • 什麼是依賴注入
    依賴注入是一種設計模式,用來解耦(Decoupling)
    不選擇在Controller Class裡面建立Service的實例(new ExampleService()),而是透過外部機制(DI 容器,內建於ASP.NET Core)將Service的實例注入給Controller。
    在ASP.NET Core中。通常是透過建構子(constructor)來進行注入(Constructor Injection)。
  • 什麼是解耦?
    解耦是「降低系統不同component或功能之間依賴性的過程」,讓component和功能可以獨立操作、修改、擴展,藉此提高可維護性和可擴展性,避免修改程式碼時「牽一髮動全身」的情況。

註冊服務

註冊服務時會在Program.cs裡使用builder.Services.AddScoped(), AddTransient(), AddSingleton()。這三個method的主要差異在於新的Service實例建立的時間點以及他們會持續多久。下方簡單介紹三個生命週期:

  • builder.Services.AddScoped():範圍型
    每個HTTP請求會建立一個實例,在同一個請求中,無論DI容器被要求多少次,都會得到同一個實例。最常用於資料庫操作和業務邏輯。因為DbContext通常也註冊為Scoped,這樣能夠確保Service和DbContext共享於同一個請求範圍內。

  • builder.Services.AddTransient():暫時型
    每次請求Service服務時都會建立一個新的實例。
    適合用來註冊輕量級、無狀態的服務,或是不能共享狀態的服務。

  • builder.Services.AddSingleton():單例型
    為整個應用程式的lifetime建立一個Service實例。
    當請求Service服務時,總是回傳同一個Service實例。
    適合用來處理共享global狀態的資源(例如:設定、快取服務等)。

實際練習:將業務邏輯移到Service

下方是一個將業務邏輯寫在Controller的範例(沒有服務層):

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
}

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    // 直接持有並操作 DbContext
    private readonly ApplicationDbContext _context;

    public ProductsController(ApplicationDbContext context)
    {
        _context = context;
    }

    // POST: api/Products/purchase/{id}
    [HttpPost("purchase/{id}")]
    public async Task<IActionResult> Purchase(int id, [FromQuery] int quantity)
    {
        if (quantity <= 0)
        {
            return BadRequest("購買數量必須大於 0。");
        }

        var product = await _context.Products.FindAsync(id);

        if (product == null)
        {
            return NotFound($"找不到 Id 為 {id} 的商品。");
        }

        if (product.Stock < quantity)
        {
            return BadRequest($"商品 {product.Name} 庫存不足。");
        }

        product.Stock -= quantity;

        await _context.SaveChangesAsync();

        return Ok(new { Message = $"成功購買 {quantity} 個 {product.Name}。", RemainingStock = product.Stock });
    }
}

來試試看改寫這個Controller, 新增服務層,建立一個ProductsService.cs檔:

namespace Products.Service
{
    public interface IProductsService {
        // interface只定義method簽名, 沒有實作
        Task<(bool IsSuccess, string Message)> PurchaseProductAsync(int id, int quantity);
    }
    
    // 實作class命名為 ProductsService,並繼承 IProductsService
    public class ProductsService: IProductsService
    {
        private readonly ApplicationDbContext _context;

        // 透過contructor注入DbContext  
        public ProductsService(ApplicationDbContext context)
        {
            _context = context;
        }

        // 實作onterface定義的method, 包含業務邏輯和資料庫操作
        public async Task<(bool IsSuccess, string Message)> PurchaseProductAsync(int id, int quantity)
        {
            if (quantity <= 0)
            {
                return (false, "購買數量必須大於 0。");
            }

            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return (false, $"找不到 Id 為 {id} 的商品。");
            }

            if (product.Stock < quantity)
            {
                return (false, $"商品 {product.Name} 庫存不足。");
            }

            product.Stock -= quantity;
            await _context.SaveChangesAsync();

            return (true, $"成功購買 {quantity} 個 {product.Name}。");
        }
    }
}

改寫原本的Controller:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    // Controller依賴於interface, 不知道service內部如何實作
    private readonly IProductsService _productsService; 
 
    public ProductsController( // 透過DI注入interface
        IProductsService productsService
    )
    {
        _productsService = productsService;
    }

    // POST: api/Products/purchase/{id}
    [HttpPost("purchase/{id}")]
    public async Task<IActionResult> Purchase(int id, [FromQuery] int quantity)
    {
        // controller只負責處理HTTP請求,呼叫業務請求
        var (isSuccess, message) = await _productsService.PurchaseProductAsync(id, quantity);
        if (!isSuccess)
        {
            return BadRequest(message);
        }
        return Ok(new { Message = message });
    }
}

完成後要記得在Program.cs中註冊服務!讓框架知道當 Controller 請求 IProductsService 時,應該提供 ProductsService 的實例。
ASP.NET Core 使用builder.Services集合來註冊應用程式所需的所有服務。
範例程式碼如下:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 註冊資料庫 DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("ExampleConnection")));

// 使用 AddScoped 註冊服務
builder.Services.AddScoped<IProductsService, ProductsService>();

builder.Services.AddControllers();

var app = builder.Build();
app.Run();

builder.Services中註冊服務時,要指定三個部分:

  1. 介面(interface): 這裡是IProductsService
  2. 實作(implementation): 服務實例,這裡是ProductsService
  3. 生命週期(lifetime):服務實例的生存時間。
  • 選擇AddScoped()的原因:
    ProductsService內部依賴於ApplicationDbContext,EF Core的DbContext在ASP.NET Core中預設也是註冊為scoped。
  • 簡單整理流程:
    1. HTTP請求進入時,會為這個請求建立一個ProductsService實例。
    2. 在請求的生命週期內,ProductsService和它依賴的ApplicationDbContext是同一個實例。
    3. 請求結束後,這些實例會被釋放,避免資源洩漏和跨請求狀態混亂。

上一篇
DAY 13 - C#中的非同步程式設計 (Asynchronous Programming)
下一篇
DAY 15 - 用C# ASP.NET Core開發一個小專案!專案發想與結構
系列文
30天的旅程!從學習C#到開發小專案15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言