Clean架构:ASP.NET Core MVC中Presenter实现与控制器注入方案咨询
Great question! Let's walk through how to implement Uncle Bob's Clean Architecture pattern in ASP.NET Core MVC, focusing on the Use Case Interactor → Presenter flow while strictly following the Dependency Inversion Principle (DIP). The short answer is yes, you should inject a use case interface into your controller, and that use case interface should depend on an abstract presenter callback interface (not a concrete implementation).
1. Lay the Foundation: Define Abstract Interfaces (DIP First)
First, we need to define our core abstractions in the inner layer of Clean Architecture (the Use Cases layer), so that outer layers (like MVC) depend on these abstractions, not the other way around.
Define the Presenter Callback Interface
This is the abstraction the use case will use to send results back. It should be technology-agnostic (no MVC-specific types here):
// Use Cases Layer (Inner Layer) public interface IGetUserPresenter { void PresentUser(UserResponseDto response); void PresentError(string errorMessage); } // DTO for use case response (also in Use Cases Layer) public class UserResponseDto { public Guid Id { get; set; } public string FullName { get; set; } public string Email { get; set; } public DateTime JoinDate { get; set; } }
Define the Use Case Interface
This interface will be injected into the controller. It depends on our abstract presenter:
// Use Cases Layer (Inner Layer) public interface IGetUserUseCase { Task ExecuteAsync(Guid userId, IGetUserPresenter presenter); }
2. Implement the Presenter (MVC Adapter)
Now, in the outer layer (MVC Presentation Layer), we implement the presenter to convert the use case's response into an MVC-specific view model, and handle how it's presented (e.g., setting ModelState, preparing view data).
// MVC Presentation Layer (Outer Layer) public class GetUserMvcPresenter : IGetUserPresenter { private readonly ControllerBase _controller; public UserViewModel ViewModel { get; private set; } public string ErrorMessage { get; private set; } public GetUserMvcPresenter(ControllerBase controller) { _controller = controller; } public void PresentUser(UserResponseDto response) { // Map use case DTO to MVC view model ViewModel = new UserViewModel { Id = response.Id, FullName = response.FullName, Email = response.Email, FormattedJoinDate = response.JoinDate.ToString("MMMM dd, yyyy") }; } public void PresentError(string errorMessage) { ErrorMessage = errorMessage; _controller.ModelState.AddModelError(string.Empty, errorMessage); } } // MVC View Model (Presentation Layer) public class UserViewModel { public Guid Id { get; set; } public string FullName { get; set; } public string Email { get; set; } public string FormattedJoinDate { get; set; } }
3. Implement the Use Case Interactor
The interactor lives in the Use Cases layer, contains the business logic, and uses the abstract presenter to send results:
// Use Cases Layer (Inner Layer) public class GetUserUseCase : IGetUserUseCase { private readonly IUserRepository _userRepository; // Depends on abstract repo interface public GetUserUseCase(IUserRepository userRepository) { _userRepository = userRepository; } public async Task ExecuteAsync(Guid userId, IGetUserPresenter presenter) { try { var user = await _userRepository.GetByIdAsync(userId); if (user == null) { presenter.PresentError($"User with ID {userId} not found."); return; } // Map domain entity to use case DTO var response = new UserResponseDto { Id = user.Id, FullName = $"{user.FirstName} {user.LastName}", Email = user.Email, JoinDate = user.JoinDate }; presenter.PresentUser(response); } catch (Exception ex) { presenter.PresentError($"Failed to retrieve user: {ex.Message}"); } } }
4. Wire It Up in the MVC Controller
Inject the use case interface into the controller. Create an instance of the MVC presenter (or inject it), pass it to the use case, then act on the presenter's results:
// MVC Controller (Presentation Layer) public class UserController : Controller { private readonly IGetUserUseCase _getUserUseCase; public UserController(IGetUserUseCase getUserUseCase) { _getUserUseCase = getUserUseCase; } public async Task<IActionResult> Details(Guid id) { // Create the presenter tied to this controller instance var presenter = new GetUserMvcPresenter(this); // Execute the use case, passing the presenter await _getUserUseCase.ExecuteAsync(id, presenter); if (!string.IsNullOrEmpty(presenter.ErrorMessage)) { return View("Error"); } return View(presenter.ViewModel); } }
5. Configure Dependency Injection
In your Program.cs (or Startup.cs for older versions), register all the abstractions and their implementations:
// Program.cs builder.Services.AddScoped<IGetUserUseCase, GetUserUseCase>(); builder.Services.AddScoped<IUserRepository, EfUserRepository>(); // Example EF Core repo implementation // For the presenter: since it's tied to a controller instance, register as transient builder.Services.AddTransient<IGetUserPresenter, GetUserMvcPresenter>(); // Alternatively, create it manually in the controller as shown above
Why This Follows DIP & Clean Architecture
- Dependency Direction: The inner Use Cases layer depends only on abstractions, not on outer layers (MVC). The MVC layer depends on the Use Cases layer's abstractions, reversing the traditional "framework-first" dependency flow.
- Testability: You can easily mock
IGetUserPresenterandIUserRepositoryto test the use case interactor without needing MVC or a database. - Flexibility: If you later want to reuse the same use case in an API or console app, just implement a new presenter (e.g.,
GetUserApiPresenter) without changing the use case code.
内容的提问来源于stack exchange,提问作者Patrick Peters

