关于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. 编程宝库

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理