使用 FastEndpoints 来垂直切换Web API的控制器方法

发布时间 2023-12-15 13:48:15作者: 伍华聪

在我们开发项目的Web API的时候,随着项目功能要求越来越多,可能我们会为控制器基类增加越来越多的基础功能,有些功能有一定的适应性,但可能在一般的子类中用不到,而随着对控制器控制要求越来越精细,那么需要为基类或者子类增加更多的控制功能,这样随着迭代的进行,有些控制器的功能会显得越来越笨重。这个时候,一种更加灵活、轻便的Web API处理方式,对每个控制器方法的垂直切割的API框架应运而生,本篇随笔介绍的FastEndpoints 就是其中这样的一款框架,本篇随笔介绍一些FastEndpoints的基础处理方法,并通过一些基础的案例,把我们《 SqlSugar 开发框架》的一些模块进行迁移性测试,对比相关后端Web API的处理,一起分享给大家。

1、FastEndpoints介绍

FastEndpoints 是Minimal API和MVC的开发人员友好替代品,它是基于REPR设计模式(请求-端点-响应),以便创建方便且可维护的端点,几乎没有样板文件。

FastEndpoints 的性能与Minimal API 相当,甚至它更快,使用更少的内存并且每秒请求数比基准测试中的MVC控制器更高。对于比如:中间件、认证、授权、日志,依赖注入这些常用功能都支持,甚至有些还进行了加强。

设计主要是分为两种模式

分层模式:mvc、mvp、mvvm等
垂直模式:REPR设计模式
REPR设计模式就是垂直模式,系统的每个组件都是单独的一块,彼此并不影响,就像微服务那样。

MVC - 模型-视图-控制器旨在与用户界面配合使用。显然,视图是一个 UI 组件。如果您正在构建 API,则没有视图,因此您充其量使用的是 MC 模式,或者您可以将其称为模型-操作-控制器并获取 MAC 模式。关键是,你已经没有将MVC用于你的API,所以考虑一个更合适的模式应该不是一个很大的问题。

API 端点是非常独立的,每个端点都可以使用三个组件来描述:

请求(Request):终结点所需的数据形状
终结点(Endpoint):终结点在给定请求时执行的逻辑
响应(Response):终结点返回给调用方的响应
结合这三个元素,你会得到请求-端点-响应或 REPR 模式。

并非所有终结点都需要其请求或响应的实际数据,在某些情况下,不接收任何输入或仅返回 HTTP 状态代码。但是,在此模式中,空请求或响应仍然是有效的请求或响应,就像某些 MVC 操作不需要模型一样。

使用 API 端点库时,您可以将请求、终端节点和响应类型分组在一起,这样就无需在某些“视图模型”或“dtos”文件夹中四处寻找合适的类型。它减少了摩擦,使使用单个端点变得更加容易。

FastEndPoint GitHub库:https://github.com/FastEndpoints/FastEndpoints

FastEndpoints 在线文档:https://fast-endpoints.com 

2、简单例子入门

参考官方的文档介绍,我们可以很容易的创建出一个简单的类似Hello开篇的API应用。

我创建一个基于.net core的Web API项目,先把FastEndPoint的相关引用加入项目中,如下所示。

 然后在项目中的启动类代码中,我们添加相关的代码使用FastEndpoints,如下所示。

using FastEndpoints;
using FastEndpoints.Swagger;

var bld = WebApplication.CreateBuilder();
bld.Services
   .AddFastEndpoints()
   .SwaggerDocument(); 

var app = bld.Build();
app.UseFastEndpoints()
   .UseSwaggerGen(); 
app.Run();

如果需要对Swagger进行一些定制修改,可以改动如下,这里先忽略。

    .SwaggerDocument(o =>
    {
        o.DocumentSettings = s =>
        {
            s.Title = "SqlSugar框架接口API文档";
            s.Version = "v1";
        };
        o.TagDescriptions = t =>
        {
            t["Test"] = "测试接口";
            t["User"] = "用户相关接口";
        };
    })

为了简便,我们以命名控件不同,以及目录,来区分不同的Web API分组,如下所示,我们创建一个基于Test的相关API接口。

对于以前的控制器接口来说,一般可能一个控制(如TestController)会包含多个方法,如上面的Create、List方法,这里使用的是FastEndpoints,它们是把一个大型的控制器切换为一个方法一个类来处理,碎片化意味着类的增加,不过我们不需要做太多的工作,可以通过它们的一些基类来简化这个过程。

我们把WebAPI中请求的Request和Response的对象,放在一个Model类文件里面,如下代码所示。

namespace FastWebApi.Controllers.Test
{
    /// <summary>
    /// 测试请求信息
    /// </summary>
    public class TestRequest
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }

    /// <summary>
    /// 测试返回信息
    /// </summary>
    public class TestResponse
    {
        public string FullName { get; set; }
        public bool IsOver18 { get; set; }
    }
}

我们来看看基于FastEndPoints方式 生成一个Create的请求Web API方法,如下代码所示

namespace FastWebApi.Controllers.Test
{
    //客户使用标识,不用覆盖 Configure 函数
    //[HttpPost("/api/user/create")]
    //[AllowAnonymous]

    /// <summary>
    /// 创建记录
    /// </summary>
    public class Create : Endpoint<TestRequest, AjaxResponse>
    {
        public override void Configure()
        {
            Post("/test/create");
            AllowAnonymous();
        }

        public override async Task HandleAsync(TestRequest req, CancellationToken ct)
        {
            var result = new TestResponse()
            {
                FullName = req.FirstName + " " + req.LastName,
                IsOver18 = req.Age > 18
            };
            await SendAsync(result.ToAjaxResponse());
        }
    }
}

我们配置Web API方法的路由,可以通过在Configure函数中指定: Post("/test/create")

也可以通过Attribute属性标识的方式,来声明,上面的注释代码所示。

[HttpPost("/api/user/create")]

这两者是等同的,任何一种方式都可以,默认的接口是需要授权才能访问的,如果我们标识了

[AllowAnonymous]

就可以匿名访问Web API 的方法了,Web API的方法处理逻辑,都是统一通过重写 HandleAsync 方法进行实现的,如上面代码所示。

其中AjaxResponse 是我定义的一个统一返回结果,这样我们的接口模型就一致了。

如下是Web API统一封装后返回的结果对象。

如果需要了解我的《SqlSugar开发框架》的统一结果返回处理,可以参考《基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用 》中的 【统一结果封装和异常处理】 部分内容即可。

如果不需要统一返回模型,则可以自定义为任何的返回类型,如下是官方的案例所示。

public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
    public override void Configure()
    {
        Post("/api/user/create");
        AllowAnonymous();
    }

    public override async Task HandleAsync(MyRequest req, CancellationToken ct)
    {
        await SendAsync(new()
        {
            FullName = req.FirstName + " " + req.LastName,
            IsOver18 = req.Age > 18
        });
    }
}

接下来,我们检查下.netcore项目的launchSettings.json 配置信息

确保打开的时候就启动Swagger页面即可。

启动Swagger页面,我们来看看具体的效果,可以看到有两个Test的接口,如下所示。

我们来调试Swagger,并测试下结果返回。

测试返回的结果如下所示,由于采用了统一返回结果的处理,这里返回的TestResponse的对象序列化信息,放在了result的里面了,如下所示。

而List的控制器方法,这里没有请求输入的对象信息,因此参数为空。具体的API方法定义如下所示。

namespace FastWebApi.Controllers.Test
{
    /// <summary>
    /// 获取所有记录
    /// </summary>
    [HttpGet("/test/list")]
    [AllowAnonymous]
    public class List : EndpointWithoutRequest<AjaxResponse>
    {
        /// <summary>
        /// 处理返回
        /// </summary>
        public override async Task HandleAsync(CancellationToken ct)
        {
            var result = new List<TestResponse>()
            {
                new TestResponse
                {
                     FullName= "test",
                      IsOver18 = true,
                },
                new TestResponse
                {
                    FullName= "test 2",
                      IsOver18 = false,
                }
            };
           await SendAsync(result.ToAjaxResponse());
        }
    }
}

Swagger接口展示界面效果。

正常执行返回结果如下所示。

 如果处理过程中有异常,由于我们采用了统一返回结果处理,因此异常信息也需要统一在对象里面,返回结果如下所示。

 以上就是简单类型的一些处理例子,结合了统一返回结果的处理,我们可以很好的定义一个通用的结果返回。

 

3、对我们SqlSugar框架常规CRUD等基类接口进行垂直切割的处理

 上面我们为了更好理解FastEndpoints的碎片化接口的处理,我们做了两个简单的方法来测试。

下面我们通过对我们SqlSugar开发框架中的基类接口进行功能上的拆分,并结合实际业务的需要接口,进行扩展的处理,从而也实现了常规CRUD的操作接口,并实现特殊业务类的API接口处理。

关于Web API的常规接口处理 ,我们为了简化代码,往往抽象一些常规的CRUD方法在控制器基类中,这样可以极大的减少了继承子类的接口代码,通过继承基类,子类自动具备了CRUD的处理接口,只需要根据业务的需要,增加一些特殊的业务接口即可。

以前的处理方法,我们是根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能,如下界面所示。

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

而现在采用FastEndpoints ,需要垂直切割整个控制器,我们需要把基类的接口实现放到具体的业务API类里面,我们为了方便,可以给他们不同的名称一个接口,或者组合在一个文件里面,如下所示。

我们来看看其中给一个简单的Count方法接口实现。

namespace FastWebApi.Controllers.User
{
    /// <summary>
    /// 根据条件计算记录数量
    /// </summary>
    [HttpGet("/user/count")]
    public class Count : Endpoint<UserPagedDto, AjaxResponse>
    {
        /// <summary>
        /// 处理请求
        /// </summary>
        public override async Task HandleAsync(UserPagedDto req, CancellationToken ct)
        {
            var result = await BLLFactory<IUserService>.Instance.CountAsync(req);
            await SendAsync(result.ToAjaxResponse());
        }
    }
}

这里可以采用接口注入的方式,也可以采用我们辅助类BLLFactory<IUserService>.Instance方法调用接口,一样的实现。

这样结合了业务的具体Service来处理,只需要简单的处理下即可,也算比较方便,由于这些基础的CRUD的方法,主要路由、分页对象,业务对象,主键类型的不同,这些可以通过我们的代码生成工具的处理快速生成即可,因此可以实现批量化的业务类的API接口方法生成。

至于具体的业务接口API,我们就需要手工处理了,如对于用户的登陆获取token的方法,我们这里需要模仿来生成一个EndPiont类,如下所示。

    /// <summary>
    /// 根据用户名、密码验证用户身份有效性
    /// </summary>
    [HttpPost("/user/verify-user")]
    [AllowAnonymous]
    public class VerifyUser : Endpoint<VerifyUserDto, AjaxResponse>
    {
        /// <summary>
        /// 处理请求
        /// </summary>
        public override async Task HandleAsync(VerifyUserDto input, CancellationToken ct)
        {
            var result = await BLLFactory<IUserService>.Instance.VerifyUser(input.UserName, input.UserPassword, input.SystemType, input.IP, input.MacAddr);
            await SendAsync(result.ToAjaxResponse());
        }
    }

其他业务方法也是类似的处理,这里的FastEndPoints的处理类,只是增加了一个简单的包装层就可以了,最后看看这些方法在SwaggerUI中的展示,和我们普通模式的Web API中的Swagger UI界面类似的效果。

 这样,我们可以在保持接口一致性的情况下,无缝的对接新的Web API接口后端了。