上次写了一篇 Minimal API Todo Sample,有些童鞋觉得 Minimal API 有些鸡肋,有一些功能的支持都不太好,但是其实 Host 之前支持的功能 Minimal API 大部分都是支持的,上次的 Todo Sample 完全没有使用 Controller 来使用 API,但也是可以使用 Controller 的,这一点从新的项目模板就能看的出来
使用 dotnet new webapi -n Net6TestApi
新的 ASP.NET Core Web API 模板项目结构如下创建新的项目,结构如下:
主要变化的结构如下:
默认启用了可空引用类型(
)和隐式命名空间引用(
)(可以参考项目文件的变化)
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 新特性所以还是之前的项目模板
上面是一个模板的变化,对于已有的项目如何做项目升级呢?
以之前的一个 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 = "[email protected]";
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 = "[email protected]" }
});
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 = "[email protected]" }
});
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 = "[email protected]";
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
Minimal API 会有一些限制,比如
不能通过 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())
更多可以参考 David 总结的一个迁移指南 https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d
Minimal API 结合了原来的 Startup,不再有 Startup,但是原来的应用也可以不必迁移到 Minimal API,根据自己的需要进行选择
https://github.com/WeihanLi/SparkTodo/commit/d3e327405c0f151e89378e9c01acde4648a7812f
https://github.com/WeihanLi/SparkTodo
https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d