随着 .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 相对够用,如果不想用或者用不了也没关系,能最终实现须要的后果就好。
参考资料:
- C# 教程
- 编程宝库