乐趣区

关于c#:详解-Net6-Minimal-API-的使用方式

随着 .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 匹配 truefalse. 疏忽大小写
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 字符串必须由一个或多个 az 的字母字符组成,且不辨别大小写。
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 相对够用,如果不想用或者用不了也没关系,能最终实现须要的后果就好。

参考资料

  1. C# 教程
  2. 编程宝库
退出移动版