为什么Microsoft仍然坚持使用ApiController?

作者:微信公众号:【架构师老卢】
3-19 19:16
44

概述:C# 及其 .NET Core 框架的发展令人瞩目,成为健壮应用程序和服务的顶级选择。然而,当我随着它的发展而发展时,一个反复出现的问题出现了:“在REST API和领域驱动设计(DDD)原则占主导地位的时代,为什么Microsoft仍然提倡使用ApiControllers?当你考虑到“控制器”这个术语在这个以领域为中心的世界中往往看起来不合适时,这个难题就变得更加令人兴奋了。MVC 和控制器的角色在传统的 Web 开发中,模型-视图-控制器 (MVC) 架构非常突出。此框架中的控制器具有明确定义的角色,将模型(数据)与视图 (UI) 桥接在一起。这可确保将用户输入转换为模型或视图的命令。在处

C# 及其 .NET Core 框架的发展令人瞩目,成为健壮应用程序和服务的顶级选择。然而,当我随着它的发展而发展时,一个反复出现的问题出现了:“在REST API和领域驱动设计(DDD)原则占主导地位的时代,为什么Microsoft仍然提倡使用ApiControllers?当你考虑到“控制器”这个术语在这个以领域为中心的世界中往往看起来不合适时,这个难题就变得更加令人兴奋了。

MVC 和控制器的角色

在传统的 Web 开发中,模型-视图-控制器 (MVC) 架构非常突出。此框架中的控制器具有明确定义的角色,将模型(数据)与视图 (UI) 桥接在一起。这可确保将用户输入转换为模型或视图的命令。在处理 UI 交互时,控制器确实很有意义,并且是 MVC 构造不可或缺的一部分。

然而,当焦点转移到 DDD 时,我们深入研究了软件的核心,即其领域——对错综复杂的业务逻辑和复杂性的细致反映。这里的方法是关于理解和建模无处不在的语言、聚合、实体和值对象。这与控制器通常居住的以 UI 为中心的世界不同。

但是,当从以 UI 为中心的 MVC 范式过渡到数据驱动的 REST 范式时,控制器的角色变得不那么明确。

RESTful 系统中的控制器

此外,在 RESTful API 领域,传统的 MVC“视图”明显缺失。在这里,叙述是关于数据交换的,而不是关于数据如何呈现或可视化。因此,在这种情况下使用“控制器”在语义上感觉不一致。我们真正谈论的是管理数据请求和响应,这个角色与 MVC 控制器的既定职责并不完全一致。

教程中的谨慎故事

也就是说,一个简单的在线搜索将产生无数的Microsoft教程和示例,其中许多描述了直接嵌入在控制器中的逻辑。虽然这种方法可能适用于教学演示或绝对的初学者,但它并不是构建可扩展的实际应用程序的基准。对可维护性、可测试性和关注点分离的担忧成为最前沿。

在探索高效且可缩放的 Web API 时,我们将深入研究 .NET Core 令人兴奋的开发:最小 API。

在许多方面,这种方法更符合 RESTful 原则和 DDD。但在我们进入代码示例和插图之前,让我们讨论一下为什么 Microsoft 仍在推广 ApiController 的使用,以及为什么是时候切换了。

但还有另一个领域,命名法似乎具有误导性——所谓的“最小 API”。

关于“最小 API”的潜在误解

ASP.NET Core 提供了两种不同的 API 开发方法:成熟的基于控制器的方法和新兴的“最小 API”范式。传统方法依赖于从 ControllerBase 扩展的类,而最小 API 则通过 lambda 或独立方法促进端点定义。乍一看,“最小 API”一词可能意味着一个精简或功能较弱的工具集,这意味着一个只为基本要素量身定制的更精简的版本。

但Microsoft的真正意图是什么?它为开发人员提供了一种直接且简化的方式来管理路由和端点,更多地关注路由机制,而不是降低整体 API 功能。考虑到这一点,像“API 路由器”或“端点路由器”这样的术语不是更具描述性,封装其核心功能而不暗示任何限制吗?

对于那些冒险进入 ASP.NET Core 的人来说,“最小 API”的标签可能会被误解为一个有限的功能——这表明可能在可用性或效率方面做出妥协。这种误解可以直接追溯到术语。名称具有权力,尤其是在以清晰和精确为基础的领域。

对于像 Microsoft 这样的行业领导者来说,选择能够准确反映其功能的术语至关重要。术语“最小 API”虽然不正确,但无意中掩盖了它的全部潜力。

让我们进入一些代码示例,以进一步说明我的观点,超越语义视角。

基于控制器的方法,控制器中有逻辑

这是我们在许多教程中看到的,尤其是在使用 Microsoft 源时。

[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly AppDbContext _dbContext;

    public OrdersController(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    [HttpPost("{orderId}/process")]
    public IActionResult ProcessOrder(int orderId)
    {
        var order = _dbContext.Orders.Find(orderId);
        if (order == null)
        {
            return NotFound();
        }

        // Apply some domain logic to validate the order
        if (!IsValidForProcessing(order))
        {
            return BadRequest("Order cannot be processed.");
        }

        // More domain logic, like updating order status, inventory, etc.
        order.Status = OrderStatus.Processed;  // Assuming an enum OrderStatus
        _dbContext.SaveChanges();

        return Ok("Order processed successfully.");
    }

    private static Boolean IsValidForProcessing(Order order)
    {
        // Validation Code for the Order.
        return result;
    }
}

上述方法的问题

  1. 违反关注点分离:在控制器中嵌入数据库访问和业务逻辑会混合不同的关注点。
  2. **降低可测试性:**此设计使单元测试具有挑战性,因为如果不涉及数据库,就无法独立测试控制器逻辑。
  3. 可伸缩性问题:随着应用程序的增长,控制器的逻辑变得臃肿,使应用程序更难维护。

具有类似陷阱的最小 API 实现

app.MapPost("/orders/{orderId}/process", (int orderId) =>
{
    var order = dbContext.Orders.Find(orderId);
    if (order == null)
    {
        return Results.NotFound();
    }

    // Apply some domain logic to validate the order
    if (!order.IsValidForProcessing())  // Assuming IsValidForProcessing method on Order
    {
        return Results.BadRequest("Order cannot be processed.");
    }

    // More domain logic, like updating order status, inventory, etc.
    order.Status = OrderStatus.Processed;  // Assuming an enum OrderStatus
    dbContext.SaveChanges();

    return Results.Ok("Order processed successfully.");
});

虽然最小 API 版本更简洁,但它仍然具有与端点定义直接相关的逻辑,呈现出上述相同的问题。

使用最少的 API 改进的方法

通过引入服务层,我们可以将应用程序逻辑与 API 端点分开,无论我们使用的是控制器还是最小 API。

public interface IOrderService
{
    bool ProcessOrder(int orderId);
}

public class OrderService : IOrderService
{
    private readonly AppDbContext _dbContext;

    public OrderService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public bool ProcessOrder(int orderId)
    {
        var order = _dbContext.Orders.Find(orderId);
        if (order == null)
        {
            throw new ArgumentException("Order not found.");
        }

        if (!order.IsValidForProcessing())
        {
            return false;
        }

        order.Status = OrderStatus.Processed;
        _dbContext.SaveChanges();

        return true;
    }
}

这是修改后的Program.cs。通过将订单处理委托给“orderService.ProcessOrder”方法,我们确保我们的 API 保持轻量级,并将特定于域的逻辑封装在服务中。这促进了单一责任原则,因为 API 端点只关心处理 HTTP 请求并返回适当的响应。

using Microsoft.EntityFrameworkCore;  
using Microsoft.Extensions.DependencyInjection;  
//... other relevant usings  
  
var builder = WebApplication.CreateBuilder(args);  
  
// Set up services, DB context, and other dependencies  
builder.Services.AddDbContext<AppDbContext>(options =>   
    options.UseYourDatabaseProvider(connectionString));   
builder.Services.AddScoped<IOrderService, OrderService>();  
  
var app = builder.Build();  
  
app.MapPost("/orders/{orderId}/process", async ([FromServices] IOrderService orderService, int orderId) =>   
{  
    try  
    {  
        bool success = await orderService.ProcessOrder(orderId);  
        if (!success)  
        {  
            return Results.BadRequest("Order cannot be processed.");  
        }  
  
        return Results.Ok("Order processed successfully.");  
    }  
    catch (ArgumentException)  
    {  
        return Results.NotFound();  
    }  
});  
  
app.Run();

这种方法可确保:

  1. 逻辑是分离的:通过将逻辑移动到服务中,我们确保 API 层保持精简,只专注于处理请求。
  2. 可维护性:随着应用程序的增长,添加新功能或更改现有功能将变得更加容易。
  3. 增强的可测试性:可以独立于 API 层对服务进行测试。

随着讨论的深入,我们深入研究了最小 API 的架构优势,很明显,我们越是分离关注点,我们的应用程序就越干净。虽然_Program.cs_中端点的直接映射简单明了,但随着应用程序的增长,可能会导致设置混乱。那么,如果有一种方法可以保持最小 API 的优雅,但采用更有条理的方法呢?

让我们引入一个结构改进,进一步将路由逻辑与核心应用程序设置分离。通过将我们的路由划分为专用类,我们可以在不影响最小 API 提供的简单性的情况下实现更好的模块化。让我带您完成此增强功能。

var app = builder.Build();  
  
// Register all endpoint mappings  
OrderEndpoints.Map(app);  
// ... any other endpoint mapping classes  
  
app.Run();

通过这样做,您正在建立一个既干净又可扩展的结构,以相同的效率满足小型和大型项目的需求。对于那些喜欢有组织和模块化代码库的人来说,这是一个很好的方法。

在检查 ASP.NET 核心 Web API 时,控制器仍然具有相关性,但最小 API 引入了重大改进。然而,命名约定有时会掩盖它们的真正潜力。通过仔细考虑架构模式并专注于关注点的分离,我们可以利用两全其美的优势。Microsoft 从 ApiController 到最小 API 的旅程证明了该行业的持续发展。通过了解这些变化并批评我们领域的巨头,开发人员可以为未来开辟更清晰的道路。无论采用何种方法,确保我们的代码保持模块化、可维护和可测试性至关重要。

相关留言评论
昵称:
邮箱:
阅读排行