.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 模板项目结构如下创建新的项目,结构如下:
主要变化的结构如下:
默认启用了可空引用类型(
<Nullable>enable</Nullable>
)和隐式命名空间引用(<ImplicitUsings>enable</ImplicitUsings>
)(可以参考项目文件的变化)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<Startup>();}).ConfigureLogging(loggingBuilder =>{loggingBuilder.AddJsonConsole();}).Build();using (var serviceScope = host.Services.CreateScope())
{var dbContext = serviceScope.ServiceProvider.GetRequiredService<SparkTodoDbContext>();await dbContext.Database.EnsureCreatedAsync();//init Database,you can add your init data herevar userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<UserAccount>>();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
{/// <summary>/// StartUp/// </summary>public class Startup{public Startup(IConfiguration configuration){Configuration = configuration.ReplacePlaceholders();}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){// Add framework services.services.AddDbContextPool<SparkTodo.Models.SparkTodoDbContext>(options => options.UseInMemoryDatabase("SparkTodo"));//services.AddIdentity<SparkTodo.Models.UserAccount, SparkTodo.Models.UserRole>(options =>{options.Password.RequireLowercase = false;options.Password.RequireUppercase = false;options.Password.RequireNonAlphanumeric = false;options.Password.RequiredUniqueChars = 0;options.User.RequireUniqueEmail = true;}).AddEntityFrameworkStores<SparkTodo.Models.SparkTodoDbContext>().AddDefaultTokenProviders();// Add JWT token validationvar 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<JWT.TokenOptions>(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) claimValidateIssuer = true,ValidIssuer = tokenIssuer,// Validate the JWT Audience (aud) claimValidateAudience = true,ValidAudience = tokenAudience,// Validate the token expiryValidateLifetime = true,// If you want to allow a certain amount of clock drift, set that here:ClockSkew = System.TimeSpan.FromMinutes(2)};});// Add MvcFrameworkservices.AddControllers();// Add api version// https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspxservices.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-authorizationservices.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<ApiVersionAttribute>().SelectMany(attr => attr.Versions);return versions.Any(v => $"v{v}" == docName);});option.OperationFilter<RemoveVersionParameterOperationFilter>();option.DocumentFilter<SetVersionInPathDocumentFilter>();// include document fileoption.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<string>() }});});services.AddHealthChecks();// Add application services.services.AddSingleton<ITokenGenerator, TokenGenerator>();//Repositoryservices.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-nameidentifieJwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();// Emit dotnet runtime version to response headerapp.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 endpointapp.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<SparkTodo.Models.SparkTodoDbContext>(options => options.UseInMemoryDatabase("SparkTodo"));
//
builder.Services.AddIdentity<SparkTodo.Models.UserAccount, SparkTodo.Models.UserRole>(options =>
{options.Password.RequireLowercase = false;options.Password.RequireUppercase = false;options.Password.RequireNonAlphanumeric = false;options.Password.RequiredUniqueChars = 0;options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<SparkTodo.Models.SparkTodoDbContext>().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<SparkTodo.API.JWT.TokenOptions>(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) claimValidateIssuer = true,ValidIssuer = tokenIssuer,// Validate the JWT Audience (aud) claimValidateAudience = true,ValidAudience = tokenAudience,// Validate the token expiryValidateLifetime = 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<ApiVersionAttribute>().SelectMany(attr => attr.Versions);return versions.Any(v => $"v{v}" == docName);});option.OperationFilter<RemoveVersionParameterOperationFilter>();option.DocumentFilter<SetVersionInPathDocumentFilter>();// include document fileoption.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<string>() }});
});
builder.Services.AddHealthChecks();
// Add application services.
builder.Services.AddSingleton<ITokenGenerator, TokenGenerator>();
//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<SparkTodoDbContext>();await dbContext.Database.EnsureCreatedAsync();//init Database,you can add your init data herevar userManager = serviceScope.ServiceProvider.GetRequiredService<UserManager<UserAccount>>();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
More
Minimal API 会有一些限制,比如
不能通过
builder.WebHost.UseStartup<Startup>()
通过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())
更多可以参考 David 总结的一个迁移指南 https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d
Minimal API 结合了原来的 Startup,不再有 Startup,但是原来的应用也可以不必迁移到 Minimal API,根据自己的需要进行选择
References
https://github.com/WeihanLi/SparkTodo/commit/d3e327405c0f151e89378e9c01acde4648a7812f
https://github.com/WeihanLi/SparkTodo
https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d