MinimalWebApi
常用
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"];
// ...
});
参数绑定
- 自动
- 使用 Fromxxxx
- 使用 TryParse
- 使用 ValueTask
- 使用 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) => {});
从 head 中获取参数
// 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
返回内容
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
-
在终结点处理程序前后运行代码。
-
检查和修改终结点处理程序调用期间提供的参数。
-
截获终结点处理程序的响应行为。
授权
使用 [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();