.NET 6 迁移到 Minimal API
Intro 上次写了一篇 Minimal API Todo Sample,有些童鞋觉得 Minimal API 有些鸡肋,有一些功能的支持都不太好,但是其实 Host 之前支持的功能 Minimal API 大部分都是支持的,上次的 Todo Sample 完全没有使用 Controller 来使用 API,但也是可以使用 Controller 的,这一点从新的项目模板就能看的出来
New Template 使用 dotnet new webapi -n Net6TestApi
新的 ASP.NET Core Web API 模板项目结构如下创建新的项目,结构如下:
文章图片
主要变化的结构如下:
- 默认启用了可空引用类型(
)和隐式命名空间引用(enable
)(可以参考项目文件的变化)enable
Program.cs
- 和之前项目的相比,新的项目模板没有了
Startup
,服务都在Program.cs
中注册
Program
使用了 C# 9 中引入的顶级应用程序以及依赖 C# 10 带来的 Global Usings 的隐式命名空间引用
- 和之前项目的相比,新的项目模板没有了
WeatherForecast
/WeatherForecastController
使用 C# 10 的 File Scoped Namespace 新特性以及上述的隐式命名空间引用
namespace Net6TestApi; public class WeatherForecast { public DateTime Date { get; set; }public int TemperatureC { get; set; }public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string? Summary { get; set; } }
dotnet new webapi -o Net5TestApi -f net5.0
可以创建 .NET 5.0 的一个 API,因为 .NET 5.0 默认不支持 C# 10 新特性所以还是之前的项目模板文章图片
Migration 上面是一个模板的变化,对于已有的项目如何做项目升级呢?
以之前的一个 TodoApp 为例,升级到 .NET 6 之后向 Minimal API 做迁移的一个示例:
修改之前的代码是这样的:
Program.cs
,比默认模板多了 Runtime metrics 的注册和数据库和默认用户的初始化using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using SparkTodo.API;
using SparkTodo.Models;
DotNetRuntimeStatsBuilder.Customize()
.WithContentionStats()
.WithGcStats()
.WithThreadPoolStats()
.StartCollecting();
var host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder.UseStartup();
})
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.AddJsonConsole();
})
.Build();
using (var serviceScope = host.Services.CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetRequiredService();
await dbContext.Database.EnsureCreatedAsync();
//init Database,you can add your init data here
var userManager = serviceScope.ServiceProvider.GetRequiredService>();
var email = "weihanli@outlook.com";
if (await userManager.FindByEmailAsync(email) == null)
{
await userManager.CreateAsync(new UserAccount
{
UserName = email,
Email = email
}, "Test1234");
}
}await host.RunAsync();
Startup
代码如下:using System;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Prometheus;
using SparkTodo.API.Services;
using SparkTodo.API.Swagger;
using SparkTodo.DataAccess;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SparkTodo.API
{
///
/// StartUp
///
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration.ReplacePlaceholders();
}
public IConfiguration Configuration { get;
}
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContextPool(options => options.UseInMemoryDatabase("SparkTodo"));
//
services.AddIdentity(options =>
{
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredUniqueChars = 0;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
// Add JWT token validation
var secretKey = Configuration.GetAppSetting("SecretKey");
var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(secretKey));
var tokenAudience = Configuration.GetAppSetting("TokenAudience");
var tokenIssuer = Configuration.GetAppSetting("TokenIssuer");
services.Configure(options =>
{
options.Audience = tokenAudience;
options.Issuer = tokenIssuer;
options.ValidFor = TimeSpan.FromHours(2);
options.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
});
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = tokenIssuer,
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = tokenAudience,
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = System.TimeSpan.FromMinutes(2)
};
});
// Add MvcFramework
services.AddControllers();
// Add api version
// https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx
services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = ApiVersion.Default;
options.ReportApiVersions = true;
});
// swagger
// https://stackoverflow.com/questions/58197244/swaggerui-with-netcore-3-0-bearer-token-authorization
services.AddSwaggerGen(option =>
{
option.SwaggerDoc("spark todo", new OpenApiInfo
{
Version = "v1",
Title = "SparkTodo API",
Description = "API for SparkTodo",
Contact = new OpenApiContact() { Name = "WeihanLi", Email = "weihanli@outlook.com" }
});
option.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "API V1" });
option.SwaggerDoc("v2", new OpenApiInfo { Version = "v2", Title = "API V2" });
option.DocInclusionPredicate((docName, apiDesc) =>
{
var versions = apiDesc.CustomAttributes()
.OfType()
.SelectMany(attr => attr.Versions);
return versions.Any(v => $"v{v}" == docName);
});
option.OperationFilter();
option.DocumentFilter();
// include document file
option.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{typeof(Startup).Assembly.GetName().Name}.xml"), true);
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Description = "Please enter into field the word 'Bearer' followed by a space and the JWT value",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
});
option.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ new OpenApiSecurityScheme
{
Reference = new OpenApiReference()
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
}, Array.Empty() }
});
});
services.AddHealthChecks();
// Add application services.
services.AddSingleton();
//Repository
services.RegisterAssemblyTypesAsImplementedInterfaces(t => t.Name.EndsWith("Repository"),
ServiceLifetime.Scoped, typeof(IUserAccountRepository).Assembly);
}public void Configure(IApplicationBuilder app)
{
// Disable claimType transform, see details here https://stackoverflow.com/questions/39141310/jwttoken-claim-name-jwttokentypes-subject-resolved-to-claimtypes-nameidentifie
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
// Emit dotnet runtime version to response header
app.Use(async (context, next) =>
{
context.Response.Headers["DotNetVersion"] = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
await next();
});
//Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
//Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint
app.UseSwaggerUI(option =>
{
option.SwaggerEndpoint("/swagger/v2/swagger.json", "V2 Docs");
option.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs");
option.RoutePrefix = string.Empty;
option.DocumentTitle = "SparkTodo API";
});
app.UseRouting();
app.UseCors(builder=>
{
builder.AllowAnyHeader().AllowAnyMethod().AllowCredentials().SetIsOriginAllowed(_=>true);
});
app.UseHttpMetrics();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
endpoints.MapMetrics();
endpoints.MapControllers();
});
}
}
}
使用 Minimal API 改造后是下面这样的:
DotNetRuntimeStatsBuilder.Customize()
.WithContentionStats()
.WithGcStats()
.WithThreadPoolStats()
.StartCollecting();
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddJsonConsole();
// Add framework services.
builder.Services.AddDbContextPool(options => options.UseInMemoryDatabase("SparkTodo"));
//
builder.Services.AddIdentity(options =>
{
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredUniqueChars = 0;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
// Add JWT token validation
var secretKey = builder.Configuration.GetAppSetting("SecretKey");
var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(secretKey));
var tokenAudience = builder.Configuration.GetAppSetting("TokenAudience");
var tokenIssuer = builder.Configuration.GetAppSetting("TokenIssuer");
builder.Services.Configure(options =>
{
options.Audience = tokenAudience;
options.Issuer = tokenIssuer;
options.ValidFor = TimeSpan.FromHours(2);
options.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
});
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = tokenIssuer,
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = tokenAudience,
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = System.TimeSpan.FromMinutes(2)
};
});
// Add MvcFramework
builder.Services.AddControllers();
// Add api version
// https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = ApiVersion.Default;
options.ReportApiVersions = true;
});
// swagger
// https://stackoverflow.com/questions/58197244/swaggerui-with-netcore-3-0-bearer-token-authorization
builder.Services.AddSwaggerGen(option =>
{
option.SwaggerDoc("spark todo", new OpenApiInfo
{
Version = "v1",
Title = "SparkTodo API",
Description = "API for SparkTodo",
Contact = new OpenApiContact() { Name = "WeihanLi", Email = "weihanli@outlook.com" }
});
option.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "API V1" });
option.SwaggerDoc("v2", new OpenApiInfo { Version = "v2", Title = "API V2" });
option.DocInclusionPredicate((docName, apiDesc) =>
{
var versions = apiDesc.CustomAttributes()
.OfType()
.SelectMany(attr => attr.Versions);
return versions.Any(v => $"v{v}" == docName);
});
option.OperationFilter();
option.DocumentFilter();
// include document file
option.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"), true);
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Description = "Please enter into field the word 'Bearer' followed by a space and the JWT value",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
});
option.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ new OpenApiSecurityScheme
{
Reference = new OpenApiReference()
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
}, Array.Empty() }
});
});
builder.Services.AddHealthChecks();
// Add application services.
builder.Services.AddSingleton();
//Repository
builder.Services.RegisterAssemblyTypesAsImplementedInterfaces(t => t.Name.EndsWith("Repository"),
ServiceLifetime.Scoped, typeof(IUserAccountRepository).Assembly);
var app = builder.Build();
// Disable claimType transform, see details here https://stackoverflow.com/questions/39141310/jwttoken-claim-name-jwttokentypes-subject-resolved-to-claimtypes-nameidentifie
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
// Emit dotnet runtime version to response header
app.Use(async (context, next) =>
{
context.Response.Headers["DotNetVersion"] = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
await next();
});
//Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
//Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint
app.UseSwaggerUI(option =>
{
option.SwaggerEndpoint("/swagger/v2/swagger.json", "V2 Docs");
option.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs");
option.RoutePrefix = string.Empty;
option.DocumentTitle = "SparkTodo API";
});
app.UseRouting();
app.UseCors(builder =>
{
builder.AllowAnyHeader().AllowAnyMethod().AllowCredentials().SetIsOriginAllowed(_ => true);
});
app.UseHttpMetrics();
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/health");
app.MapMetrics();
app.MapControllers();
using (var serviceScope = app.Services.CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetRequiredService();
await dbContext.Database.EnsureCreatedAsync();
//init Database,you can add your init data here
var userManager = serviceScope.ServiceProvider.GetRequiredService>();
var email = "weihanli@outlook.com";
if (await userManager.FindByEmailAsync(email) == null)
{
await userManager.CreateAsync(new UserAccount
{
UserName = email,
Email = email
}, "Test1234");
}
}
await app.RunAsync();
改造方法:
- 原来 Program 里的
Host.CreateDefaultBuilder(args)
使用新的var builder = WebApplication.CreateBuilder(args);
来代替
- 原来
Program
里的ConfigureLogging
使用builder.Logging
来配置builder.Logging.AddJsonConsole();
- 原来
Program
里的ConfigureAppConfiguration
使用builder.Configuration.AddXxx
来配置builder.Configuration.AddJsonFile("");
- 原来
Startup
里的服务注册使用builder.Services
来注册
- 原来
Startup
里的配置是从构造器注入的,需要使用配置的话用builder.Configuration
来代替
- 原来
Startup
里中间件的配置,通过var app = builder.Build();
构建出来的WebApplication
来注册
- 原来
Program
里的host.Run
/host.RunAsync
需要改成app.Run
/app.RunAsync
- 不能通过
builder.WebHost.UseStartup()
通过Startup
来注册服务和中间件的配置的
- 不能通过
builder.Host.UseEnvironment
/builder.Host.UseContentRoot
/builder.WebHost.UseContentRoot
/builder.WebHost.UseEnvironment
/builder.WebHost.UseSetting
来配置 host 的一些配置
- 现在的
WebApplication
实现了IEndpointRouteBuilder
,可以不用UseEndpoints
来注册,比如可以直接使用app.MapController()
代替app.UseEndpoints(endpoints => endpoints.MapController())
Minimal API 结合了原来的 Startup,不再有 Startup,但是原来的应用也可以不必迁移到 Minimal API,根据自己的需要进行选择
References
- https://github.com/WeihanLi/SparkTodo/commit/d3e327405c0f151e89378e9c01acde4648a7812f
- https://github.com/WeihanLi/SparkTodo
- 【c#|.NET 6 迁移到 Minimal API】https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d
推荐阅读
- C#|详解 .Net6 Minimal API 的使用方式
- qiao索引器
- C|C语言中的短路现象
- 人工智能|HTTP调试工具(Fiddler介绍一(翻译))
- C#检查两个ArrayList对象是否相等
- halcon|halcon联合C#的实时采集显示
- 笔记|顺序表的实现