Home

Awesome

Biwen.QuickApi

Nuget Nuget GitHub license PRs Welcome

项目介绍

临时文档地址 <br/> Biwen.QuickApi 2+,是一个微型aspnetcore开发框架,提供minimalapi的QuickApi封装,提供IQuickEndpoint书写minimalapi, 模块化支持Modular,多租户,发布订阅:IEvent,作业调度:IScheduleTask,审计:Auditing,缓存,LocalLock,OpenApi ~~

public class MyStore
{
    public static Todo[] SampleTodos()
    {
        return [
            new(1, "Walk the dog"),
            new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
            ];
    }
}

[QuickApi("todos")] //返回对象方式
public class TodoApi : BaseQuickApi<EmptyRequest,Todo[]>
{
    public override async ValueTask<Todo[]> ExecuteAsync(EmptyRequest request)
    {
        await Task.CompletedTask;
        return MyStore.SampleTodos();
    }
}

开发工具

依赖环境&库

使用方式

Step0 Nuget

dotnet add package Biwen.QuickApi

Step1 UseBiwenQuickApis

BiwenQuickApiOptions配置项:

services.AddBiwenQuickApis(Action<BiwenQuickApiOptions>? options);//add services
app.UseBiwenQuickApis();//use middleware

Step2 Define Request and Response


public class HelloApiRequest : BaseRequest<HelloApiRequest>
{
    [Description("Name Desc")]
    public string? Name { get; set; }

    /// <summary>
    /// FromQuery特性绑定字段
    /// </summary>
    [FromQuery("q")]
    public string? Q { get; set; }
    public HelloApiRequest()
    {
        RuleFor(x => x.Name).NotNull().Length(5, 10);
    }
}
    
/// <summary>
/// 上传文件FileUploadRequest 
/// </summary>
public class FileUploadRequest : BaseRequest<FileUploadRequest>
{
    public IFormFile? File { get; set; }

    public FileUploadRequest()
    {
        RuleFor(x => x.File).NotNull();
    }
}

/// <summary>
/// 模拟自定义绑定的Request
/// </summary>
public class CustomApiRequest : BaseRequest<CustomApiRequest>
{
    public string? Name { get; set; }

    public CustomApiRequest()
    {
        RuleFor(x => x.Name).NotNull().Length(5, 10);
    }
}
/// <summary>
/// 标记FromBody,表示这个请求对象是FromBody的
/// </summary>
[FromBody]
public class FromBodyRequest : BaseRequest<FromBodyRequest>
{
    public int Id { get; set; }
    public string? Name { get; set; }

    public FromBodyRequest()
    {
        RuleFor(x => x.Id).InclusiveBetween(1, 100);//必须1~100
    }
}
/// <summary>
/// 自定义的绑定器
/// </summary>
public class CustomApiRequestBinder : IReqBinder<CustomApiRequest>
{
    public static async ValueTask<CustomApiRequest> BindAsync(HttpContext context,ParameterInfo parameter = null)
    {
        var request = new CustomApiRequest
        {
            Name = context.Request.Query["c"]
        };
        await Task.CompletedTask;
        return request;
    }
}

public class HelloApiResponse
{
    public string? Message { get; set; }
}

Step3 Define QuickApi


/// <summary>
/// get ~/admin/index
/// </summary>
[QuickApi("index", Group = "admin", Verbs = Verb.GET | Verb.POST, Policy = "admin")]
[QuickApiSummary("this is summary","this is description")]
public class NeedAuthApi : BaseQuickApi
{
    public override IResult Execute(EmptyRequest request)
    {
        return Results.Ok();
    }
}

/// <summary>
/// get ~/hello/world/{name}
/// </summary>
[QuickApi("world/{name}", Group = "hello", Verbs = Verb.GET | Verb.POST)]
public class HelloApi : BaseQuickApi<HelloApiRequest, HelloApiResponse>
{
    private readonly HelloService _service;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public HelloApi(HelloService service,IHttpContextAccessor httpContextAccessor)
    {
        _service = service;
        _httpContextAccessor = httpContextAccessor;
    }

    public override HelloApiResponse Execute(HelloApiRequest request)
    {
        var hello = _service.Hello($"hello world {_httpContextAccessor.HttpContext!.Request.Path} !");
        return new HelloApiResponse{ Message = hello };
    }
}

/// <summary>
/// get ~/custom?c=11112222
/// </summary>
[QuickApi("custom", Verbs = Verb.GET)]
public class CustomApi : BaseQuickApi<CustomApiRequest>
{
    public CustomApi()
    {
        //自定义绑定器
        UseReqBinder<CustomApiRequestBinder>();
    }

    public override async ValueTask<IResult> ExecuteAsync(CustomApiRequest request)
    {
        await Task.CompletedTask;
        Console.WriteLine($"获取自定义的 CustomApi:,从querystring:c绑定,{request.Name}");
        return Results.Ok();
    }

    /// <summary>
    /// 提供minimal扩展
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public override RouteHandlerBuilder HandlerBuilder(RouteHandlerBuilder builder)
    {
        //自定义描述
        builder.WithOpenApi(operation => new(operation)
        {
            Summary = "This is a summary",
            Description = "This is a description"
        });

        //自定义标签
        builder.WithTags("custom");

        //自定义过滤器
        builder.AddEndpointFilter(async (context, next) =>
        {
            Console.WriteLine("自定义过滤器!");
            return await next(context);
        });
        //默认实现了Accepts和Produces
        return base.HandlerBuilder(builder);
        //如果完全自定义直接返回Builder
        //return builder;
        }
}
    
/// <summary>
/// 上传文件测试
/// 请使用postman & apifox 测试
/// </summary>
[QuickApi("fromfile", Verbs = Verb.POST)]
[QuickApiSummary("上传文件测试", "上传文件测试")]
public class FromFileApi : BaseQuickApi<FileUploadRequest, Results<Ok<string>,BadRequest<string>>>
{
    public override async ValueTask<Results<Ok<string>,BadRequest<string>> ExecuteAsync(FileUploadRequest request)
    {
        //测试上传一个文本文件并读取内容
        if (request.File != null)
        {
            using (var sr = new StreamReader(request.File.OpenReadStream()))
            {
                var content = await sr.ReadToEndAsync();
                return TypedResults.Ok(content);
            }
        }
        return TypedResults.BadRequest("no file");
    }
}

/// <summary>
/// JustAsService 只会被服务发现,不会被注册到路由表
/// </summary>
[QuickApi(""), JustAsService]
public class JustAsService : BaseQuickApi<EmptyRequest, string>
{
    public override async ValueTask<string> ExecuteAsync(EmptyRequest request)
    {
        return "Hello World JustAsService!";
    }
}

提供QuickApi的Group扩展支持


   // 当前模拟给所有 Group为空的QuickApi加上 Tag "Def" 
    public class MyGroupRouteBuilder : IQuickApiGroupRouteBuilder
    {
        // 表述Group为空的QuickApi
        public string Group => string.Empty;
        // 执行顺序
        public int Order => 1;
        // 实现Builder方法
        public RouteGroupBuilder Builder(RouteGroupBuilder routeBuilder)
        {
            // 给所有 Group为空的QuickApi加上 Tag "Def"
            routeBuilder.WithTags("Def");
            return routeBuilder;
        }
    }

// 最后注册
builder.Services.AddBiwenQuickApiGroupRouteBuilder<MyGroupRouteBuilder>();

Step4 Enjoy


//直接访问
// GET ~/hello/world/biwen
// GET ~/hello/world/biwen?name=biwen
// POST ~/hello/world/biwen
// GET ~/custom?c=11112222


//你也可以把QuickApi当Service使用
app.MapGet("/fromapi", async (Apis.Hello4Api api) =>
{
    //通过你的方式获取请求对象
    var req = new EmptyRequest();
    //验证请求对象
    var result = req.RealValidator.Validate(req);
    if (!result.IsValid)
    {
        return Results.BadRequest(result.ToDictionary());
    }
    //执行请求
    var x = await api.ExecuteAsync(new EmptyRequest());
    return Results.Ok(x);
});

Step5 OpenApi集成


//register openapi & quickapi document
builder.Services.AddOpenApi(options =>
{
    options.UseTransformer<BearerSecuritySchemeTransformer>();
    options.ShouldInclude = (desc) => true;
});

//more doc group...

//map openapi doc & ui
app.MapGroup("openapi", app =>
{
    //swagger ui
    app.MapOpenApi("{documentName}.json");
    app.MapScalarUi();
});

Step6 OpenApi 以及Client代理


/// <summary>
/// refit client
/// </summary>
public interface IBusiness
{
    [Refit.Get("/fromapi")]
    public Task<TestRsp> TestPost();
}

//Refit
builder.Services.AddRefitClient<IBusiness>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5101"));

var app = builder.Build();

app.MapGet("/from-quickapi", async (IBusiness bussiness) =>
{
    var resp = await bussiness.TestPost();
    return Results.Content(resp.Message);
});

Benchmark性能测试

BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.3570/22H2/2022Update)
11th Gen Intel Core i7-11800H 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100
[Host]     : .NET 8.0.0 (8.0.0.100), X64 RyuJIT AVX2 [AttachedDebugger]
Job-WHDDIT : .NET 8.0.0 (8.0.0.100), X64 RyuJIT AVX2

Runtime=.NET 8.0  InvocationCount=2000  IterationCount=10  
LaunchCount=1  WarmupCount=1  

MethodMeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
WebApiCtrl385.5 μs357.93 μs236.75 μs231.0 μs1.000.002.500033.5 KB1.00
MinimalApi221.2 μs13.02 μs6.81 μs220.9 μs0.680.342.000024.38 KB0.73
QuickApi235.9 μs22.26 μs11.65 μs235.4 μs0.720.342.000027.59 KB0.82

Q&A

var md = context.GetEndpoint()?.Metadata.GetMetadata<QuickApiMetadata>();
if (md == null || md.QuickApiType == null)
{
    await _next(context);
    return;
}

//todo: