Skip to content

MinimalWebApi

最小 API 快速参考 | Microsoft Learn

常用

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 加入配置
builder.Configuration.AddIniFile("appsettings.ini");

// 或是简写
// var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

// 可以从不同的地方指定端口
// 从 Properties/launchSettings.json 中载入配置(含端口)
// dotnet run --urls="https://localhost:7777"
// ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
// app.Urls.Add("http://localhost:3000");
// app.Run("http://localhost:3000");

// 载入证书
// appsettings.json 中
// builder.Configuration 中
// builder.WebHost.ConfigureKestrel 中

app.Run();

feature 环境变量 命令行参数
应用程序名称 ASPNETCORE_APPLICATIONNAME --applicationName
环境名称 ASPNETCORE_ENVIRONMENT --environment
内容根 ASPNETCORE_CONTENTROOT --contentRoot
// 1. 通用类型参数  Map{Verb}
app.MapGet("api/v1/process", () => "this is get").WithGroupName("v1"); // 将 api WithGroupName 分至不同的 swaager 版本中
app.MapPost("api/v2/process", () => "this is post").WithGroupName("v2");
app.MapPut("process", () => "this is put");
app.MapDelete("process", () => "this is delete");
app.MapMethods("allProcess", new[] { HttpMethod.Delete.Method, HttpMethod.Get.Method }, () => "this is all process");

// 2. 带参数 
app.MapGet("api/parseint", (int idx) => new { idx });
app.MapPost("api/postparam", ([FromBody] PostParam param) => param); // 参数 boyd 中用 json 传入

//  [FromQuery(Name = "p")] 可使用 name 指定参数来源名称 
app.MapGet("api/queryparam", ([FromQuery] PostParam param) => param); // 从 query 中传入参数 

/*app.MapPost("api/formparam", ([FromForm] PostParam param) => param); // 使用 form 传入,  */

// FromHeader(Name = "X-CUSTOM-HEADER") 使用 name 指定参数来源名称 
app.MapGet("api/getparam", ([FromHeader] PostParam param) => param); // 参数从 head 中传入,但是参数中实现一个 static bool TryParse(string, T) 函数, 见下面例子 
// 2. 2使用注入参数 
builder.Services.AddSingleton<PostParam>((_) => new PostParam() { Idx = -1, Message = $"我是注入来的 {DateTime.Now}" });
app.MapPost("api/serviceparam", ([FromServices] PostParam param) => param); // 注入, 


// 3. 标准返回值 
app.MapPost("api/404", () => Results.NotFound());
app.MapPost("api/forbid", () => Results.Forbid());
app.MapPost("api/badrequest", () => Results.BadRequest());
app.MapPost("api/unauthorized", Results.Unauthorized);
app.MapPost("api/file", () => Results.File("xxx.jpg", MediaTypeNames.Image.Jpeg, "xxxx.jpg" ));

// 4. 全部捕获
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

// 5. 使用 LinkGenerator 动态调用,下面转发请示至一个名为 hi 的函数进行处理
app.MapGet("/", (LinkGenerator liner) => $@"{liner.GetPathByName("hi")}"); // 查找名为  hi 的函数进行调用 
app.MapGet("/hello", () => "hello named route").WithName("hi"); // 为调用一个名称为 name

// 6. 路由参数 
/*app.MapPost("api/routeparam", ([FromRoute] PostParam param) => param); // 路由参数, */
app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

// 从原始的 HttpRequest 中读取参数
app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

参数绑定

  1. 自动
  2. 使用 Fromxxxx
  3. 使用 TryParse
  4. 使用 ValueTask
  5. 使用 AsParameters
// 直接绑定参数 
app.MapGet("api/fromheader", ([FromHeader] PostParam param) => param); // head 中 加入  -H 'param: idx,0,message,string', 调用 PostParam.TryParse
app.MapGet("api/fromquery", ([FromQuery] PostParam? param) => param);  // 调用  https://localhost:7150/api/fromquery?param=test , 调用 PostParam.TryParse
app.MapGet("api/bindasync", (PostParam? postParam) => postParam); // 没有指定数据来源时调用  PostParam.ValueTask 进行解析 
app.MapGet("api/fromroute/{id:int}", ([FromRoute] int id) => id);
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

public class PostParam
{
    public int Idx { get; set; }

    public string Message { get; set; } = string.Empty;

    // 为了 FromHeader 与 fromquery 进行解析
    public static bool TryParse(string data, IFormatProvider provider, out PostParam param)
    {
        param = new PostParam { Idx = 2222, Message = data ?? "null data" };
        return true;
    }

    // 使用 BindAsync 优先级更高 
    public static async ValueTask<PostParam?> BindAsync(HttpContext context)
    {
        return new PostParam();
    }
}


// 将绑定的参数赋值给一个对象(该对象中可以绑定注入对象), 调用比如参数中传入值 https://localhost:7150/api/asparameters?Idx=13123&Message=3333
app.MapPost("api/asparameters", ([AsParameters] WorkData workData) => workData);

internal class WorkData
{
    public int Idx { get; set; }
    public string Message { get; set; }
    // 可以直接绑定注入的对象
    public ILogger<Program> Logger { get; set; }
}

指定路由参数

// 指定参数类型
//  所有参数 
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
// int 类型
app.MapGet("api/routdata/{id:int}", (int id) => $@"get {id} int");
// str 类型
app.MapGet("api/routdata/{id:maxlength(12)}", (string id) => $@"get {id} string");
// 正则使用 /posts/{slug:regex(^[a-z0-9_-]+$)}  

绑定至数组

// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) => $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) => $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues., StringValues 似乎带有字符串枚举功能
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) => $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// GET /todoitems/tags?tags=home&tags=work, 绑定至对象数组,对象需要实现 TryParse 之类的函数
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>{ });

// POST /todoitems/batch, 应该是 post json 数组,数组部分被转为 Todo 数组 
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) => {});

// 从 header 中读参数
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) => {});
// 2.1 使用 head 参数 
public class PostParam
{   // 为了 FromHeader 进行解析 
    public static bool TryParse(string data, out PostParam param)
    {
        param = new PostParam() { Idx = 2222, Message = data ?? "null data" };
        return true;
    }
}
app.MapGet("api/getparam", ([FromHeader] PostParam param) => param); // 参数从 head 中传入,但是参数中实现一个 static bool TryParse(string, T) 函数

特殊参数

app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));

app.MapGet("/", (HttpRequest request, HttpResponse response) =>
    response.WriteAsync($"Hello World {request.Query["name"]}"));

// 传入关联的 token
app.MapGet("/", async (CancellationToken cancellationToken) => 
    await MakeLongRunningRequestAsync(cancellationToken));

// 传入当前登录用户
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);

绑定优先级

最小 API 快速参考 | Microsoft Learn

返回内容

最小 API 快速参考 | Microsoft Learn

Microsoft.AspNetCore.Http.Results 内置了一些 IResult 的返回结果

返回类型

返回值 行为 Content-Type
IResult 框架调用 IResult.ExecuteAsync IResult 实现决定
string 框架将字符串直接写入响应 text/plain
T(任何其他类型) 框架将 JSON 序列化响应 application/json

高级返回内容

自定义 IResult

app.MapPost("api/xoutputresult", () => new XOutputResult());

// 自定义输出容,继承自 IResult
public class XOutputResult : IResult
{
    public async Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Application.Json;

        await httpContext.Response.WriteAsJsonAsync(new { text = nameof(XOutputResult) });

    }
}

分组

将接口按类型统一处理

 app.MapGroup("/public/todos")
        .MapTodosApi() // 这里调用函数进行处理, 接口地址开头为 /public/todos/xxx
        .AddEndpointFilterFactory(QueryPrivateTodos) // 这里类似于挂入过滤器生成工厂
        .WithTags("Public"); // 这个 Tag 类型是给 swi

app.MapGroup("/private/todos")
        .MapTodosApi()
        .WithTags("Private")
        .AddEndpointFilterFactory(QueryPrivateTodos)
        .RequireAuthorization(); // 该分组下载的接口都要求登录

    // 定义接口
    public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
    {
        group.MapGet("/", GetAllTodos); //  这里为处理函数,与函数回调函数一样
        group.MapGet("/{id}", GetTodo);
        group.MapPost("/", CreateTodo);
        // group.MapPut("/{id}", UpdateTodo);
        // group.MapDelete("/{id}", DeleteTodo);

        return group;
    }

连接分组函数

        var all = app.MapGroup("").WithOpenApi();
        var org = all.MapGroup("{org}");
        var user = org.MapGroup("{user}");
        user.MapGet("", (string org, string user) => $"{org}/{user}"); // 到这里就是 /org/user/

        var manager = org.MapGroup("{manager}");
        user.MapGet("", (string org, string manager) => $"{org}/{manager}"); // 到这里就是 /org/manager/            

每一组设置处理函数

为 /outer 与 /outer/inner 设置处理函数及过滤器

        var outer = app.MapGroup("/outer");
        var inner = outer.MapGroup("/inner");

        inner.AddEndpointFilter((context, next) =>
        {
            app.Logger.LogInformation("/inner group filter");
            return next(context);
        });

        outer.AddEndpointFilter((context, next) =>
        {
            app.Logger.LogInformation("/outer group filter");
            return next(context);
        });

        outer.MapGet("/", () => "");

        inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
        {
            app.Logger.LogInformation("MapGet filter");
            return next(context);
        });

筛选器

最小 API 快速参考 | Microsoft Learn

最小 API 筛选器应用 | Microsoft Learn

  • 在终结点处理程序前后运行代码。

  • 检查和修改终结点处理程序调用期间提供的参数。

  • 截获终结点处理程序的响应行为。

授权

使用 [Authorize] 或是 RequireAuthorization(), AllowAnonymous, AllowAnonymous()

builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", b => b.RequireClaim("admin", "true")));

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/auth", () => "This endpoint requires authorization").RequireAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () => "The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.").RequireAuthorization("AdminsOnly");
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.").AllowAnonymous();

CORS

使用 EnableCors 或是 MyAllowSpecificOrigins

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

终结点筛选器

终结点筛选器

  • 传入 EndpointFilterInvocationContext 和返回 EndpointFilterDelegate

  • 实现 IEndpointFilter 接口


string ColorName(string color) => $"Color specified: {color}!";

app.MapGet("/colorSelector/{color}", ColorName)
    .AddEndpointFilter(async (invocationContext, next) =>
    {
        var color = invocationContext.GetArgument<string>(0);

        if (color == "Red")
        {
            return Results.Problem("Red not allowed!");
        }
        return await next(invocationContext);
    });
## 多级调用 
app.MapGet("/", () => { return "Test of multiple filters"; })
    .AddEndpointFilter(async (efiContext, next) =>    { return await next(efiContext); })
    .AddEndpointFilter(async (efiContext, next) =>    { return = await next(efiContext); })
    .AddEndpointFilter(async (efiContext, next) =>    { return await next(efiContext); });
app.MapGet("/", () =>    { return "Test of multiple filters"; })
    .AddEndpointFilter<AEndpointFilter>()
    .AddEndpointFilter<BEndpointFilter>()
    .AddEndpointFilter<CEndpointFilter>();

// BEndpointFilter, CEndpointFilter 与  ABCEndpointFilters 相似
public abstract class ABCEndpointFilters : IEndpointFilter
{
    public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        return await next(context);        
    }
}

终结点筛选器工厂

使用筛选器工厂可以使用 MethodInfo 中提供的某些信息。 然后再返回筛选器

    // 这里是工厂部分,应该是查询函数参数等信息,然后返回一个终结点筛选器
    // 函数是程序初始化时进行调用,返回的 终结点筛选器在函数每次调用时调用 
    private static EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext context, EndpointFilterDelegate next)
    {

        var dbContextIndex = -1;

        foreach (var argument in context.MethodInfo.GetParameters())
        {
            if (argument.ParameterType == typeof(TodoItemDataContext))
            {
                dbContextIndex = argument.Position;
                break;
            }
        }

        // Skip filter if the method doesn't have a TodoDb parameter.
        if (dbContextIndex < 0)
        {
            return next;
        }

        return async invocationContext =>
        {
            // 这里进行真实调用前的回调用
            var dbContext = invocationContext.GetArgument<TodoItemDataContext>(dbContextIndex);
            /*dbContext.IsPrivate = true;*/

            try
            {
                return await next(invocationContext);
            }
            finally
            {
                // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
                /*dbContext.IsPrivate = false;*/
            }
        };
    }

数据传输

Stream

以 stream 读取 body,下面的例子将读出的数据传入一个通道,有一个后台服务读取通过中的数据



    // 1. 注册上传服务
    public static void AddStreamData(this WebApplicationBuilder builder)
    {
        // 2. 注册上传通道读取器
        builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) => Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

        // 3. 注册上传服务(在服务中读取上传的通道读取器)
        builder.Services.AddHostedService<BackgroundQueue>();
    }

    // 使用上传服务
    public static void UseStreamData(this WebApplication app)
    {
        // 传入的 stream 为 HttpRequest.Body
        app.MapPost("/regist",  async (HttpRequest req, Stream body,  Channel<ReadOnlyMemory<byte>> queue) =>
        {
            // 判断下上传数据是否超出要求
            if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
            {
                return Results.BadRequest();
            }

            // 从上传数据中读出二进制数据
            // We're not above the message size and we have a content length, or
            // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
            // We add one to the message size so that we can detect when a chunked request body
            // is bigger than our configured max.
            var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

            var buffer = new byte[readSize];

            // Read at least that many bytes from the body.
            var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

            // 数据大于约定值时直接认为发生错误
            // We read more than the max, so this is a bad request.
            if (read > maxMessageSize)
            {
                return Results.BadRequest();
            }

            // 将上传的数据写入通道 
            // Attempt to send the buffer to the background queue.
            if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
            {
                return Results.Accepted();
            }

            // We couldn't accept the message since we're overloaded.
            return Results.StatusCode(StatusCodes.Status429TooManyRequests);
        });
    }


    // 开一个服务处理通道中的数据
    class BackgroundQueue : BackgroundService
    {
        public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,)
        {
            _queue = queue;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // 循环获取传入的数据, 注意一下处异常情况 
            await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;     
            }
        }
    }

swagger

// 为 api 指定不同版本

builder.Services.AddEndpointsApiExplorer();
/*builder.Services.AddSwaggerGen();*/

builder.Services.AddSwaggerGen((options) =>
{
    options.SwaggerDoc("v1", new OpenApiInfo() { Title = "测试项目 V1", Version = "v1" });
    options.SwaggerDoc("v2", new OpenApiInfo() { Title = "测试项目 V2", Version = "v2" });
});


    // 1.2 启用 swagger 
    app.UseSwagger();
    /*app.UseSwaggerUI();*/
    app.UseSwaggerUI(options =>
    {
        options.EnableTryItOutByDefault(); // 开启试一下按钮 
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "测试项目 V1"); // 重定义 swagger 地址 
        options.SwaggerEndpoint("/swagger/v2/swagger.json", "测试项目 V2"); // 重定义 swagger 地址 
    });

// 将 api WithGroupName 分至不同的 swaager 版本中
app.MapGet("api/v1/process", () => "this is get").WithGroupName("v1").AddTag("user");  // AddTag 对接口进行分组,
app.MapPost("api/v2/process", () => "this is post").WithGroupName("v2").addTab("manager");
app.MapPut("process", () => "this is put");

IFormFile 方式

见 《文件上传 (swagger 兼容法)》

文件上传 (swagger 兼容法)

// 单文件上传
app.MapPost("api/fileUpload", (IFormFile file) => new { file.FileName, file.Length, File.OpenWrite });

// 多文件上传
app.MapPost("api/filesUpload",
    ([FromForm] IFormFileCollection files) => { return files.Select(it => new { it.FileName, it.Length }); });

// 这个没试过与 swagger 是否兼容 
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles) { }
});

文件下载 (swagger 兼容法)


app.MapGet("api/download/{fileName}", async (string fileName, IHostEnvironment environment) =>
    {
        var mimeType = "application/zip";
        var path = Path.Combine(environment.ContentRootPath, @$"{fileName}.zip");

        var bytes = await File.ReadAllBytesAsync(path);

        return Results.File(bytes, mimeType, $"{fileName}.zip");
    })
    .Produces(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound);

有空看下

在 ASP.NET Core 中使用 HttpContext | Microsoft Learn

autofac

// 接管注册系统 
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// 注入系统 ,可以直使用 x.Register 单个 注册,也可以使用 RegisterModule 注册一个模块,模块统一管理所有注册类
builder.Host.ConfigureContainer<ContainerBuilder>(x => x.RegisterModule(new XAppModule()));

var app = builder.Build();


// 统一管理实现
public class XAppModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
       // 一个程序集中实现接口,一个实现,然后一次性注册
       var interfaceAssembly = Assembly.Load("xxx.Interface"); // 载入接口
       var serviceAssembly =   Assembly.Load("xxx.Service"); // 载入实现
       builder.RegisterAssemblyTypes(interfaceAssembly, serviceAssembly).AsImplementedInterfaces();

        base.Load(builder);
    }
}

配置

变量

ASP.NET Core 中的配置 | Microsoft Learn

优先级从高至底,从命令行传入, 从环境中读取 xxx, ASPNETCORE_xxx, DOTNET_xxx, 系统机密, appsettings.{Environment}.json, appsettings.json,

builder.Configuration.AddEnvironmentVariables(prefix: "MyCustomPrefix_"); 自定义一个环境变量的前缀

set MyKey="My key from Environment" 设置环境值

setx MyKey "My key from setx Environment" 永久设置用户变量

setx MyKey "My key from setx Environment" /M 永久设置系统变量

常用配置方法



var section = builder.Configuration.GetSection("JwtConfig");
if (section.Exists())
{
    var children = section.GetChildren();
}

// 绑定至对象
var jwtConfig = new JwtConfig();
builder.Configuration.GetSection("JwtConfig").Bind(jwtConfig);

// 返回配置对象
builder.Configuration.GetSection("JwtConfig").Get<JwtConfig>();

// 返回参数值 
builder.Configuration.GetSection("JwtConfig").GetValue<string>("key");

// 使用注入
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection(JwtConfig.ConfigName));
// 以  IOptions<JwtConfig> configOptions; AddSingleton 形式注入
// IOptionsSnapshot<JwtConfig>   scoped 注入, 每个访问过程中都是最新值
// IOptionsMonitor<JwtConfig>  AddSingleton 注入, 调用 CurrentValue 时会自动更新值 

// 当有多个节点可以使用同一个配置类时
builder.Services.Configure<JwtConfig>("secionName1",builder.Configuration.GetSection(JwtConfig.ConfigName));
builder.Services.Configure<JwtConfig>("secionName2", builder.Configuration.GetSection(JwtConfig.ConfigName));
IOptionsSnapshot<JwtConfig> cfg;
cfg.Get("secionName2");


// 当生成配置需要使用注入服务时,使用 AddOptions
builder.Services.AddOptions<JwtConfig>().Configure<IServiceProvider>((config, provide) => { config.Key = "xxx" });


// 加入验证功能, JwtConfig 需要使用注释属性 DataAnnotations, 或是实现 一个 IValidateOptions<T>, 具体查手册 
builder.Services.AddOptions<JwtConfig>().Bind(builder.Configuration.GetSection(JwtConfig.ConfigName)).ValidateDataAnnotations();