.NET WebAPI:将控制器业务逻辑抽离为带接口的Handler类
Alright, let's break down how to refactor your EmployeeDetailController to move business logic into a dedicated handler class with an interface—this will make your controller cleaner, more maintainable, and much easier to test with xUnit. Here's the step-by-step solution:
IEmployeeDetailHandler Interface First, we'll create an interface that abstracts all business operations for EmployeeDetail. This decouples the controller from the concrete implementation, making testing straightforward.
public interface IEmployeeDetailHandler { // Get all employee details IEnumerable<EmployeeDetail> GetAll(); // Get a single employee by ID Task<EmployeeDetail?> GetByIdAsync(int id); // Update an existing employee Task<(bool Success, bool IsNotFound)> UpdateAsync(EmployeeDetail employeeDetail); // Create a new employee Task<EmployeeDetail> CreateAsync(EmployeeDetail employeeDetail); // Delete an employee by ID Task<EmployeeDetail?> DeleteAsync(int id); // Check if an employee exists by ID bool Exists(int id); }
EmployeeDetailHandler Class Next, build the concrete handler that implements the interface. This class directly injects AuthenticationContext (no controller dependencies) and handles all data access and business logic.
using Microsoft.EntityFrameworkCore; public class EmployeeDetailHandler : IEmployeeDetailHandler { private readonly AuthenticationContext _context; public EmployeeDetailHandler(AuthenticationContext context) { _context = context; } public IEnumerable<EmployeeDetail> GetAll() { return _context.EmployeeDetails; } public async Task<EmployeeDetail?> GetByIdAsync(int id) { return await _context.EmployeeDetails.FindAsync(id); } public async Task<(bool Success, bool IsNotFound)> UpdateAsync(EmployeeDetail employeeDetail) { _context.Entry(employeeDetail).State = EntityState.Modified; try { await _context.SaveChangesAsync(); return (true, false); } catch (DbUpdateConcurrencyException) { if (!Exists(employeeDetail.EId)) { return (false, true); } // Re-throw the exception if it's not a "not found" concurrency issue throw; } } public async Task<EmployeeDetail> CreateAsync(EmployeeDetail employeeDetail) { _context.EmployeeDetails.Add(employeeDetail); await _context.SaveChangesAsync(); return employeeDetail; } public async Task<EmployeeDetail?> DeleteAsync(int id) { var employeeDetail = await _context.EmployeeDetails.FindAsync(id); if (employeeDetail == null) { return null; } _context.EmployeeDetails.Remove(employeeDetail); await _context.SaveChangesAsync(); return employeeDetail; } public bool Exists(int id) { return _context.EmployeeDetails.Any(e => e.EId == id); } }
EmployeeDetailController Update the controller to inject IEmployeeDetailHandler instead of AuthenticationContext. The controller now only handles HTTP request validation and response formatting—all business logic is delegated to the handler.
[Route("api/[controller]")] [ApiController] public class EmployeeDetailController : ControllerBase { private readonly IEmployeeDetailHandler _handler; public EmployeeDetailController(IEmployeeDetailHandler handler) { _handler = handler; } // GET: api/EmployeeDetail [HttpGet] public IEnumerable<EmployeeDetail> GetEmployeeDetails() { return _handler.GetAll(); } // GET: api/EmployeeDetail/5 [HttpGet("{id}")] public async Task<IActionResult> GetEmployeeDetail([FromRoute] int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var employeeDetail = await _handler.GetByIdAsync(id); if (employeeDetail == null) { return NotFound(); } return Ok(employeeDetail); } // PUT: api/EmployeeDetail/5 [HttpPut("{id}")] public async Task<IActionResult> PutEmployeeDetail([FromRoute] int id, [FromBody] EmployeeDetail employeeDetail) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != employeeDetail.EId) { return BadRequest(); } var updateResult = await _handler.UpdateAsync(employeeDetail); if (updateResult.IsNotFound) { return NotFound(); } return NoContent(); } // POST: api/EmployeeDetail [HttpPost] public async Task<IActionResult> PostEmployeeDetail([FromBody] EmployeeDetail employeeDetail) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var createdEmployee = await _handler.CreateAsync(employeeDetail); return CreatedAtAction("GetEmployeeDetail", new { id = createdEmployee.EId }, createdEmployee); } // DELETE: api/EmployeeDetail/5 [HttpDelete("{id}")] public async Task<IActionResult> DeleteEmployeeDetail([FromRoute] int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var deletedEmployee = await _handler.DeleteAsync(id); if (deletedEmployee == null) { return NotFound(); } return Ok(deletedEmployee); } }
Don't forget to register the interface and its implementation in your application's DI container (usually in Program.cs):
builder.Services.AddScoped<IEmployeeDetailHandler, EmployeeDetailHandler>();
Why This Works
- Testability: You can easily mock
IEmployeeDetailHandlerfor controller tests, or mockAuthenticationContext(using an in-memory database or a framework like Moq) to test the handler's business logic with xUnit. - Separation of Concerns: The controller focuses on HTTP-related tasks (validation, status codes, response formatting), while the handler handles data access and business rules.
- Maintainability: Changes to business logic only need to be made in the handler, not scattered across controller methods.
内容的提问来源于stack exchange,提问作者Jeyamini Madara

