随着 .Net6 的发布,微软也改进了对之前 ASP.NET Core 构建方式,使用了新的 Minimal API 模式。以前默认的方式是需要在 Startup 中注册 IOC 和中间件相关,但是在 Minimal API 模式下你只需要简单的写几行代码就可以构建一个 ASP.NET Core的Web 应用,可谓非常的简单,加之配合 c# 的 global using 和 Program 的顶级声明方式,使得 Minimal API 变得更为简洁,不得不说 .NET 团队在 .NET 上近几年下了不少功夫,接下来我们就来大致介绍下这种极简的使用模式。
1. 使用方式
使用 Visual Studio 2022 新建的 ASP.NET Core 6 的项目,默认的方式就是 Minimal API 模式,整个 Web 程序的结构看起来更加简单,再i加上微软对 Lambda 的改进,使其可以对 Lambda 参数进行 Attribute 标记,有的场景甚至可以放弃去定义 Controller 类了。
2. 几行代码构建Web程序
使用 Minimal API 最简单的方式就是能通过三行代码就可以构建一个 WebApi 的程序,代码如下:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World");
app.Run();
是的你没有看错,仅仅这样运行起来就可以,默认监听的 http://localhost:5000 和 https://localhost:5001,所以直接在浏览器输入http://localhost:5000地址就可以看到浏览器输出Hello World字样。
3. 更改监听地址
如果你想更改它监听的服务端口,可以使用如下的方式:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World");
app.Run("http://localhost:6666");
如果想同时监听多个端口的话,可以使用如下的方式:
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:6666");
app.Urls.Add("http://localhost:8888");
app.MapGet("/", () => "Hello World");
app.Run();
或者是直接通过环境变量的方式设置监听信息,设置环境变量ASPNETCORE_URLS的值为完整的监听URL地址,这样的话就可以直接省略了在程序中配置相关信息了。
ASPNETCORE_URLS=http://localhost:6666
如果设置多个监听的URL地址的话可以在多个地址之间使用分号;隔开多个值:
ASPNETCORE_URLS=http://localhost:6666;https://localhost:8888
如果想监听本机所有Ip地址,可以使用如下方式:
var app = WebApplication.Create(args);
app.Urls.Add("http://*:6666");
app.Urls.Add("http://+:8888");
app.Urls.Add("http://0.0.0.0:9999");
app.MapGet("/", () => "Hello World");
app.Run();
同样的也可以使用添加环境变量的方式添加监听地址:
ASPNETCORE_URLS=http://*:6666;https://+:8888;http://0.0.0.0:9999
4. 日志操作
日志操作也是比较常用的操作,在Minimal API中微软干脆把它提出来,直接简化了操作,如下所示:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.Logger.LogInformation("读取到的配置信息:{content}", builder.Configuration.GetSection("consul").Get<ConsulOption>());
app.Run();
5. 基础环境配置
无论我们在之前的.Net Core开发或者现在的.Net6开发都有基础环境的配置,它包括 ApplicationName、ContentRootPath、 EnvironmentName相关,不过在Minimal API中,可以通过统一的方式去配置:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging
});
Console.WriteLine($"应用程序名称: {builder.Environment.ApplicationName}");
Console.WriteLine($"环境变量: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot目录: {builder.Environment.ContentRootPath}");
var app = builder.Build();
或者是通过环境变量的方式去配置,最终实现的效果都是一样的。
- ASPNETCORE_ENVIRONMENT
- ASPNETCORE_CONTENTROOT
- ASPNETCORE_APPLICATIONNAME
6. 主机相关设置
我们在之前的.Net Core开发模式中,程序的启动基本都是通过构建主机的方式,比如之前的Web主机或者后来的泛型主机,在Minimal API中同样可以进行这些操作,比如我们模拟一下之前泛型主机配置Web程序的方式:
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureDefaults(args).ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
var app = builder.Build();
如果只是配置Web主机的话Minimal API还提供了另一种更直接的方式,如下所示:
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStartup<Startup>();
builder.WebHost.UseWebRoot("webroot");
var app = builder.Build();
7. 默认容器替换
很多时候我们在使用IOC的时候会使用其他三方的IOC框架,比如大家耳熟能详的Autofac,我们之前也介绍过其本质方式就是使用UseServiceProviderFactory中替换容器的注册和服务的提供,在Minimal API中可以使用如下的方式去操作:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
//之前在Startup中配置ConfigureContainer可以使用如下方式
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
8. 中间件相关
相信大家都已经仔细看过了WebApplication.CreateBuilder(args).Build()通过这种方式构建出来的是一个WebApplication类的实例,而WebApplication正是实现了 IApplicationBuilder接口。所以其本质还是和我们之前使用Startup中的Configure方法的方式是一致的,比如我们配置一个Swagger程序为例:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
//判断环境变量
if (app.Environment.IsDevelopment())
{
//异常处理中间件
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
//启用静态文件
app.UseStaticFiles();
app.UseAuthorization();
app.MapControllers();
app.Run();
<p>常用的中间件配置还是和之前是一样的,因为本质都是IApplicationBuilder
的扩展方法,我们这里简单列举一下:</p>
中间件名称 | 描述 | API |
---|---|---|
Authentication | 认证中间件 | app.UseAuthentication() |
Authorization | 授权中间件. | app.UseAuthorization() |
CORS | 跨域中间件. | app.UseCors() |
Exception Handler | 全局异常处理中间件. | app.UseExceptionHandler() |
Forwarded Headers | 代理头信息转发中间件. | app.UseForwardedHeaders() |
HTTPS Redirection | Https重定向中间件. | app.UseHttpsRedirection() |
HTTP Strict Transport Security (HSTS) | 特殊响应头的安全增强中间件. | app.UseHsts() |
Request Logging | HTTP请求和响应日志中间件. | app.UseHttpLogging() |
Response Caching | 输出缓存中间件. | app.UseResponseCaching() |
Response Compression | 响应压缩中间件. | app.UseResponseCompression() |
Session | Session中间件 | app.UseSession() |
Static Files | 静态文件中间件. | app.UseStaticFiles() , app.UseFileServer() |
WebSockets | WebSocket支持中间件. | app.UseWebSockets() |
9. 请求处理
我们可以使用WebApplication中的Map{HTTPMethod}相关的扩展方法来处理不同方式的Http请求,比如以下示例中处理Get、Post、Put、Delete相关的请求:
app.MapGet("/", () => "Hello GET");
app.MapPost("/", () => "Hello POST");
app.MapPut("/", () => "Hello PUT");
app.MapDelete("/", () => "Hello DELETE");
如果想让一个路由地址可以处理多种Http方法的请求,可以使用MapMethods方法,如下所示:
app.MapMethods("/multiple", new[] { "GET", "POST","PUT","DELETE" }, (HttpRequest req) => $"Current Http Method Is {req.Method}" );
通过上面的示例我们不仅看到了处理不同Http请求的方式,还可以看到Minimal Api可以根据委托的类型自行推断如何处理请求,比如上面的示例,我们没有写Response Write相关的代码,但是输出的却是委托里的内容,因为我们上面示例中的委托都满足Func<string>的形式,所以Minimal Api自动处理并输出返回的信息,其实只要满足委托类型的它都可以处理,接下来咱们来简单一下,首先是本地函数的形式:
static string LocalFunction() => "This is local function";
app.MapGet("/local-fun", LocalFunction);
还可以是类的实例方法:
HelloHandler helloHandler = new HelloHandler();
app.MapGet("/instance-method", helloHandler.Hello);
class HelloHandler
{
public string Hello()
{
return "Hello World";
}
}
亦或者是类的静态方法:
app.MapGet("/static-method", HelloHandler.SayHello);
class HelloHandler
{
public static string SayHello(string name)
{
return $"Hello {name}";
}
}
其实本质都是一样的,那就是将他们转换为可执行的委托,无论什么样的形式,能满足委托的条件即可。
10. 路由约束
Minimal Api还支持在对路由规则的约束,这个和我们之前使用UseEndpoints的方式类似,比如我约束路由参数只能为整型,如果不满足的话会返回404。
app.MapGet("/users/{userId:int}", (int userId) => $"user id is {userId}");
app.MapGet("/user/{name:length(20)}", (string name) => $"user name is {name}");
经常使用的路由约束还有其他几个,也不是很多大概有如下几种,简单的列一下表格:
限制 | 示例 | 匹配示例 | 说明 |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
匹配任何整数 |
bool |
{active:bool} |
true , false |
匹配 true 或 false . 忽略大小写 |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
匹配满足DateTime 类型的值 |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
匹配满足 decimal 类型的值 |
double |
{height:double} |
1.234 , -1,001.01e8 |
匹配满足 double 类型的值 |
float |
{height:float} |
1.234 , -1,001.01e8 |
匹配满足 float 类型的值 |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
匹配满足Guid 类型的值 |
long |
{ticks:long} |
123456789 , -123456789 |
匹配满足 long 类型的值 |
minlength(value) |
{username:minlength(4)} |
KOBE |
字符串长度必须是4个字符 |
maxlength(value) |
{filename:maxlength(8)} |
CURRY |
字符串长度不能超过8个字符 |
length(length) |
{filename:length(12)} |
somefile.txt |
字符串的字符长度必须是12个字符 |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
字符串的字符长度必须介于8和l6之间 |
min(value) |
{age:min(18)} |
20 |
整数值必须大于18 |
max(value) |
{age:max(120)} |
119 |
整数值必须小于120 |
range(min,max) |
{age:range(18,120)} |
100 |
整数值必须介于18和120之间 |
alpha |
{name:alpha} |
Rick |
字符串必须由一个或多个a -z 的字母字符组成,且不区分大小写。 |
regex(expression) |
{ssn:regex(^\d{{3}}-\d{{2}}-\d{{4}}$)} |
123-45-6789 |
字符串必须与指定的正则表达式匹配。 |
required |
{name:required} |
JAMES |
请求信息必须包含该参数 |
11. 模型绑定
在我们之前使用ASP.NET Core Controller方式开发的话,模型绑定是肯定会用到的,它的作用就是简化我们解析Http请求信息也是MVC框架的核心功能,它可以将请求信息直接映射成c#的简单类型或者POCO上面。在Minimal Api的Map{HTTPMethod}相关方法中同样可以进行丰富的模型绑定操作,目前可以支持的绑定源有如下几种:
- Route(路由参数)
- QueryString
- Header
- Body(比如JSON)
- Services(即通过IServiceCollection注册的类型)
- 自定义绑定
1) 绑定示例
接下来我们首先看一下绑定路由参数:
app.MapGet("/sayhello/{name}", (string name) => $"Hello {name}");
还可以使用路由和querystring的混用方式:
app.MapGet("/sayhello/{name}", (string name,int? age) => $"my name is {name},age {age}");
这里需要注意的是,我的age参数加了可以为空的标识,如果不加的话则必须要在url的请求参数中传递age参数,否则将报错,这个和我们之前的操作还是有区别的。
具体的类也可以进行模型绑定,比如咱们这里定义了名为Goods的POCO进行演示:
app.MapPost("/goods",(Goods goods)=>$"商品{goods.GName}添加成功");
class Goods
{
public int GId { get; set; }
public string GName { get; set; }
public decimal Price { get; set; }
}
需要注意的是HTTP方法GET、HEAD、OPTIONS、DELETE将不会从body进行模型绑定,如果需要在Get请求中获取Body信息,可以直接从HttpRequest中读取它。
如果我们需要使用通过IServiceCollection注册的具体实例,可以以通过模型绑定的方式进行操作(很多人喜欢叫它方法注入,但是严格来说却是是通过定义模型绑定的相关操作实现的),而且还简化了具体操作,我们就不需要在具体的参数上进行FromServicesAttribute标记了。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<Person>(provider => new() { Id = 1, Name = "yi念之间", Sex = "Man" });
var app = builder.Build();
app.MapGet("/", (Person person) => $"Hello {person.Name}!");
app.Run();
如果是混合使用的话,也可以不用指定具体的BindSource进行标记了,前提是这些值的名称在不同的绑定来源中是唯一的,这种感觉让我想到了刚开始学习MVC4.0的时候模型绑定的随意性,比如下面的例子:
app.MapGet("/sayhello/{name}", (string name,int? age,Person person) => $"my name is {name},age {age}, sex {person.Sex}");
上面示例的模型绑定参数来源可以是:
参数 | 绑定来源 |
---|---|
name | 路由参数 |
age | querystring |
person | 依赖注入 |
不仅仅如此,它还支持更复杂的方式,这使得模型绑定更为灵活,比如以下示例:
app.MapPost("/goods",(Goods goods, Person person) =>$"{person.Name}添加商品{goods.GName}成功");
它的模型绑定的值来源可以是:
参数 | 绑定来源 |
---|---|
goods | body里的json |
person | 依赖注入 |
当然如果你想让模型绑定的来源更清晰,或者就想指定具体参数的绑定来源那也是可以的,反正就是各种灵活,比如上面的示例改造一下,这样就可以显示声明:
app.MapPost("/goods",([FromBody]Goods goods, [FromServices]Person person) =>$"{person.Name}添加商品{goods.GName}成功");
很多时候我们可能通过定义类和方法的方式来声明Map相关方法的执行委托,这个时候呢依然可以进行灵活的模型绑定,而且可能你也发现了,直接通过lambda表达式的方式虽然支持可空类型,但是它不支持缺省参数,也就是咱们说的方法默认参数的形式,比如:
app.MapPost("/goods", GoodsHandler.AddGoods);
class GoodsHandler
{
public static string AddGoods(Goods goods, Person person, int age = 20) => $"{person.Name}添加商品{goods.GName}成功";
}
当然你也可以对AddGoods方法的参数进行显示的模型绑定处理,十分的灵活。
public static string AddGoods([FromBody] Goods goods, [FromServices] Person person, [FromQuery]int age = 20) => $"{person.Name}添加商品{goods.GName}成功";
在使用Map相关方法的时候,由于是在Program入口程序或者其他POCO中直接编写相关逻辑的,因此需要用到HttpContext、HttpRequest、HttpResponse相关实例的时候没办法进行直接操作,这个时候也需要通过模型绑定的方式获取对应实例:
app.MapGet("/getcontext",(HttpContext context,HttpRequest request,HttpResponse response) => response.WriteAsync($"IP:{context.Connection.RemoteIpAddress},Request Method:{request.Method}"));
2) 自定义绑定
Minimal Api采用了一种新的方式来自定义模型绑定,这种方式是一种基于约定的方式,无需提前注册,也无需集成什么类或者实现什么接口,只需要在自定义的类中存在TryParse和BindAsync方法即可,这两个方法的区别如下:
TryParse方法是对路由参数、url参数、header相关的信息进行转换绑定
BindAsync可以对任何请求的信息进行转换绑定,功能比TryParse要强大
接下来我们分别演示一下这两种方式的使用方法,首先是TryParse方法:
app.MapGet("/address/getarray",(Address address) => address.Addresses);
public class Address
{
public List<string>? Addresses { get; set; }
public static bool TryParse(string? addressStr, IFormatProvider? provider, out Address? address)
{
var addresses = addressStr?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (addresses != null && addresses.Any())
{
address = new Address { Addresses = addresses.ToList() };
return true;
}
address = new Address();
return false;
}
}
这样就可以完成简单的转换绑定操作,从写法上我们可以看到,TryParse方法确实存在一定的限制,不过操作起来比较简单,这个时候我们模拟请求:
http://localhost:5036/address/getarray?address=山东,山西,河南,河北
请求完成会得到如下结果:
["山东","山西","河南", "河北"]
然后我们改造一下上面的例子使用BindAsync的方式进行结果转换,看一下它们操作的不同:
app.MapGet("/address/getarray",(Address address) => address.Addresses);
public class Address
{
public List<string>? Addresses { get; set; }
public static ValueTask<Address?> BindAsync(HttpContext context, ParameterInfo parameter)
{
string addressStr = context.Request.Query["address"];
var addresses = addressStr?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
Address address = new();
if (addresses != null && addresses.Any())
{
address.Addresses = addresses.ToList();
return ValueTask.FromResult<Address?>(address);
}
return ValueTask.FromResult<Address?>(address);
}
}
同样请求http://localhost:5036/address...山东,山西,河南,河北 地址会得到和上面相同的结果,到底如何选择同学们可以按需使用,得到的效果都是一样的。如果类中同时存在TryParse和BindAsync方法,那么只会执行BindAsync方法。
输出结果:
相信通过上面的其他示例演示,我们大概看到了一些在Minimal Api中的结果输出,总结起来其实可以分为三种情况:
IResult 结果输出,可以包含任何值得输出,包含异步任务Task<IResult>和ValueTask<IResult>
string 文本类型输出,包含异步任务Task<string>和ValueTask<string>
T 对象类型输出,比如自定义的实体、匿名对象等,包含异步任务 Task<T>和ValueTask<T>
接下来简单演示几个例子来简单看一下具体是如何操作的,首先最简单的就是输出文本类型:
app.MapGet("/hello", () => "Hello World");
然后输出一个对象类型,对象类型可以包含对象或集合甚至匿名对象,或者是咱们上面演示过的HttpResponse对象,这里的对象可以理解为面向对象的那个对象,满足Response输出要求即可。
app.MapGet("/simple", () => new { Message = "Hello World" });
//或者是
app.MapGet("/array",()=>new string[] { "Hello", "World" });
//亦或者是EF的返回结果
app.Map("/student",(SchoolContext dbContext,int classId)=>dbContext.Student.Where(i=>i.ClassId==classId));
还有一种是微软帮我们封装好的一种形式,即返回的是IResult类型的结果,微软也是很贴心的为我们统一封装了一个静态的Results类,方便我们使用,简单演示一下这种操作:
//成功结果
app.MapGet("/success",()=> Results.Ok("Success"));
//失败结果
app.MapGet("/fail", () => Results.BadRequest("fail"));
//404结果
app.MapGet("/404", () => Results.NotFound());
//根据逻辑判断返回
app.Map("/student", (SchoolContext dbContext, int classId) => {
var classStudents = dbContext.Student.Where(i => i.ClassId == classId);
return classStudents.Any() ? Results.Ok(classStudents) : Results.NotFound();
});
上面我们也提到了Results类其实是微软帮我们多封装了一层,它里面的所有静态方法都是返回IResult的接口实例,这个接口有许多实现的类,满足不同的输出结果,比如Results.File("foo.text")方法,本质就是返回一个FileContentResult类型的实例。
public static IResult File(byte[] fileContents,string? contentType = null,
string? fileDownloadName = null,
bool enableRangeProcessing = false,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue? entityTag = null)
=> new FileContentResult(fileContents, contentType)
{
FileDownloadName = fileDownloadName,
EnableRangeProcessing = enableRangeProcessing,
LastModified = lastModified,
EntityTag = entityTag,
};
亦或者Results.Json(new { Message="Hello World" }),本质就是返回一个JsonResult类型的实例。
public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
=> new JsonResult
{
Value = data,
JsonSerializerOptions = options,
ContentType = contentType,
StatusCode = statusCode,
};
当然我们也可以自定义IResult的实例,比如我们要输出一段html代码。
微软很贴心的为我们提供了专门扩展Results的扩展类IResultExtensions基于这个类我们才能完成IResult的扩展:
static class ResultsExtensions
{
//基于IResultExtensions写扩展方法
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions, nameof(resultExtensions));
//自定义的HtmlResult是IResult的实现类
return new HtmlResult(html);
}
}
class HtmlResult:IResult
{
//用于接收html字符串
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
/// <summary>
/// 在该方法写自己的输出逻辑即可
/// </summary>
/// <returns></returns>
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
定义完成这些我们就可以直接在Results类中使用我们定义的扩展方法了,使用方式如下:
app.MapGet("/hello/{name}", (string name) => Results.Extensions.Html(@$"<html>
<head><title>Index</title></head>
<body>
<h1>Hello {name}</h1>
</body>
</html>"));
这里需要注意的是,我们自定义的扩展方法一定是基于IResultExtensions扩展的,然后再使用的时候注意是使用的Results.Extensions这个属性,因为这个属性是IResultExtensions类型的,然后就是我们自己扩展的Results.Extensions.Html方法。
12. 总结
本文我们主要是介绍了ASP.NET Core 6 Minimal API的常用的使用方式,相信大家对此也有了一定的了解,在.NET6中也是默认的项目方式,整体来说却是非常的简单、简洁、强大、灵活,不得不说Minimal API却是在很多场景都非常适用的。当然我也在其它地方看到过关于它的评价,褒贬不一吧,笔者认为,没有任何一种技术是银弹,存在即合理。如果你的项目够规范够合理,那么使用Minimal API绝对够用,如果不想用或者用不了也没关系,能最终实现需要的结果就好。
参考资料:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。