哈囉大家好!
今天要來介紹ASP.NET Core中的服務層(Service Layer)以及依賴注入(DI)。
服務層是位於Controller和資料庫存取(EF Core Context)之間的中間層!
服務層的功能主要是用來分離職責!
Controller應該只要負責處理HTTP請求,例如:驗證請求格式、呼叫服務)。
服務層則是負責處理業務邏輯(Business Logic),例如:資料庫操作、計算、驗證規則...等。
然後資料庫層(EF Core)專門負責資料的增刪查改。
透過分層的方式,可以提高程式碼的可讀性和可擴展性,也讓程式碼更易於測試(獨立測試業務邏輯)。
註冊服務時會在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狀態的資源(例如:設定、快取服務等)。
下方是一個將業務邏輯寫在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
中註冊服務時,要指定三個部分:
IProductsService
。ProductsService
。AddScoped()
的原因:ProductsService
內部依賴於ApplicationDbContext
,EF Core的DbContext
在ASP.NET Core中預設也是註冊為scoped。ProductsService
實例。ProductsService
和它依賴的ApplicationDbContext
是同一個實例。