在此之前,写过一篇 给新手的WebAPI实践 ,获得了很多新人的认可,那时还是基于.net mvc,文档生成还是自己闹洞大开写出来的,经过这两年的时间,netcore的发展已经势不可挡,自己也在不断的学习,公司的项目也转向了netcore。大部分也都是前后分离的架构,后端api开发居多,从中整理了一些东西在这里分享给大家。 源码地址:https://gitee.com/loogn/NetApiStarter,这是一个基于netcore mvc 3.0的模板项目,如果你使用的netcore 2.x,除了引用不通用外,代码基本是可以复用的。下面介绍一下其中的功能。 登录验证 这里我默认使用了jwt登录验证,因为它足够简单和轻量,在netcore mvc中使用jwt验证非常简单,首先在startup.cs文件中配置服务并启用: ConfigureServices方法中: var jwtSection = Configuration.GetSection("Jwt"); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = jwtSection["Audience"], ValidIssuer = jwtSection["Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["SigningKey"])) }; }); Configure方法中,在UseRouting和UseEndpoints方法之前: app.UseAuthorization(); 上面我们使用到了jwt配置块,对应appsettings.json文件中有这样的配置: { "Jwt": { "SigningKey": "1234567812345678", "Issuer": "NetApiStarter", "Audience": "NetApiStarter" } } 我们再操作两步来实现登录验证, 一、提供一个接口生成jwt, 二、在客户端请求头部加上Authorization: Bearer {jwt} 我先封装了一个生成jwt的方法 public static class JwtHelper { public static string WriteToken(Dictionary claimDict, DateTime exp) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppSettings.Instance.Jwt.SigningKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: AppSettings.Instance.Jwt.Issuer, audience: AppSettings.Instance.Jwt.Audience, claims: claimDict.Select(x => new Claim(x.Key, x.Value)), expires: exp, signingCredentials: creds); var jwt = new JwtSecurityTokenHandler().WriteToken(token); return jwt; } } 然后在登录服务中调用 /// /// 登录,获取jwt /// /// /// public ResultObject Login(LoginRequest request) { var user = userDao.GetUser(request.Account, request.Password); if (user == null) { return new ResultObject("用户名或密码错误"); } var dict = new Dictionary(); dict.Add("userid", user.Id.ToString()); var jwt = JwtHelper.WriteToken(dict, DateTime.Now.AddDays(7)); var response = new LoginResponse { Jwt = jwt }; return new ResultObject(response); } 在Controller和Action上添加[Authorize]和[AllowAnonymous]两个特性就可以实现登录验证了。 请求响应 这里请求响应的设计依然没有使用restful风格,一是感觉太麻烦,二是真的不太懂(实事求是),所以请求还是以POST方式投递JSON数据,响应当然也是JSON数据这个没啥异议的。 为啥使用POST+JSON呢,主要是简单,大家都懂,而且规则统一、繁简皆宜,比如什么参数都不需要,就传{},根据ID查询文章{articleId:23},或者复杂的查询条件和表单提交{ name:'abc', addr:{provice:'HeNan', city:'ZhengZhou'},tags:['骑马','射箭'] } 等等都可以优雅的传递。 这只是我个人的风格,netcore mvc是支持其他的方式的,选自己喜欢的就行了。 下面的内容还是按照POST+JSON来说。 首先提供请求基类: /// /// 登录用户请求的基类 /// public class LoginedRequest { #region jwt相关用户 private ClaimsPrincipal _claimsPrincipal { get; set; } public ClaimsPrincipal GetPrincipal() { return _claimsPrincipal; } public void SetPrincipal(ClaimsPrincipal user) { _claimsPrincipal = user; } public string GetClaimValue(string name) { return _claimsPrincipal?.FindFirst(name)?.Value; } #endregion #region 数据库相关用户 (如果有必要的话) //不用属性是因为swagger中会显示出来 private User _user; public User GetUser() { return _user; } public void SetUser(User user) { _user = user; } #endregion } 这个类中说白了就是两个手写属性,一个ClaimsPrincipal用来保存从jwt解析出来的用户,一个User用来保存数据库中完整的用户信息,为啥不直接使用属性呢,上面注释也提到了,不想在api文档中显示出来。这个用户信息是在服务层使用的,而且User不是必须的,比如jwt中的信息够服务层使用,不定义User也是可以的,总之这里的信息是为服务层逻辑服务的。 我们还可以定义其他的基类,比如经常用的分页基类: public class PagedRequest : LoginedRequest { public int PageIndex { get; set; } public int PageSize { get; set; } } 根据项目的实际情况还可以定义更多的基类来方便开发。 响应类使用统一的格式,这里直接提供json方便查看: { "result": { "jwt": "string" }, "success": true, "code": 0, "msg": "错误信息" } result是具体的响应对象,如果success为false的话,result一般是null。 ActionFilter mvc本身是一个扩展性极强的框架,层层有拦截,ActionFilter就是其中之一,IActionFilter接口有两个方法,一个是OnActionExecuted,一个是OnActionExecuting,从命名也能看出,就是在Action的前后分别执行的方法。我们这里主要重写OnActionExecuting方法来做两件事: 一、将登陆信息赋值给请求对象 二、验证请求对象 这里说的请求对象,其类型就是LoginedRequest或者LoginedRequest的子类,看代码: [AppService] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class MyActionFilterAttribute : ActionFilterAttribute { /// /// 是否验证参数有效性 /// public bool ValidParams { get; set; } = true; public override void OnActionExecuting(ActionExecutingContext context) { //由于Filters是套娃模式,使用以下逻辑保证作用域的覆盖 Action > Controller > Global if (context.Filters.OfType().Last() != this) { return; } //默认只有一个参数 var firstParam = context.ActionArguments.FirstOrDefault().Value; if (firstParam != null && firstParam.GetType().IsClass) { //验证参数合法性 if (ValidParams) { var validationResults = new List(); var validationFlag = Validator.TryValidateObject(firstParam, new ValidationContext(firstParam), validationResults, false); if (!validationFlag) { var ro = new ResultObject(validationResults.First().ErrorMessage); context.Result = new JsonResult(ro); return; } } } var requestParams = firstParam as LoginedRequest; if (requestParams != null) { //设置jwt用户 requestParams.SetPrincipal(context.HttpContext.User); var userid = requestParams.GetClaimValue("userid"); //如果有必要,可以每次都获取数据库中的用户 if (!string.IsNullOrEmpty(userid)) { var user = ((UserService)context.HttpContext.RequestServices.GetService(typeof(UserService))).SingleById(long.Parse(userid)); requestParams.SetUser(user); } } base.OnActionExecuting(context); } } 模型验证这块使用的是系统自带的,从上面代码也可以看出,如果请求对象定义为LoginedRequest及其子类,每次请求会填充ClaimsPrincipal,如果有必要,可以从数据库中读取User信息填充。 请求经过ActionFilter时,模型验证不通过的,直接返回了验证错误信息,通过之后到达Action和Service时,用户信息已经可以直接使用了。 api文档和日志 api文档首选swagger了,aspnetcore 官方文档也是使用的这个,我这里用的是Swashbuckle,首先安装引用 Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc4 定义一个扩展类,方便把swagger注入容器中: public static class SwaggerServiceExtensions { public static IServiceCollection AddSwagger(this IServiceCollection services) { //https://github.com/domaindrivendev/Swashbuckle.AspNetCore services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My Api", Version = "v1" }); c.IgnoreObsoleteActions(); c.IgnoreObsoleteProperties(); c.DocumentFilter(); //自定义类型映射 c.MapType(() => new OpenApiSchema { Type = "byte", Example = new OpenApiByte(0) }); c.MapType(() => new OpenApiSchema { Type = "long", Example = new OpenApiLong(0L) }); c.MapType(() => new OpenApiSchema { Type = "integer", Example = new OpenApiInteger(0) }); c.MapType(() => new OpenApiSchema { Type = "DateTime", Example = new OpenApiDateTime(DateTimeOffset.Now) }); //xml注释 foreach (var file in Directory.GetFiles(AppContext.BaseDirectory, "*.xml")) { c.IncludeXmlComments(file); } //Authorization的设置 c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "请输入验证的jwt。示例:Bearer {jwt}", Name = "Authorization", Type = SecuritySchemeType.ApiKey, }); }); return services; } /// /// Swagger控制器描述文字 /// class SwaggerDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { swaggerDoc.Tags = new List { new OpenApiTag{ Name="User", Description="用户相关"}, new OpenApiTag{ Name="Common", Description="公共功能"}, }; } } } 主要是验证部分,加上去之后就可以在文档中使用jwt测试了 然后在startup.cs的ConfigureServices方法中 services.AddSwagger(); Configure方法中: if (env.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); options.DocExpansion(DocExpansion.None); }); } 这里限制了只有在开发环境才显示api文档,如果是需要外部调用的话,可以不做这个限制。 日志组件使用Serilog。 首先也是安装引用 Install-Package Serilog Install-Package Serilog.AspNetCore Install-Package Serilog.Settings.Configuration Install-Package Serilog.Sinks.RollingFile 然后在appsettings.json中添加配置 { "Serilog": { "WriteTo": [ { "Name": "Console" }, { "Name": "RollingFile", "Args": { "pathFormat": "logs/{Date}.log" } } ], "Enrich": [ "FromLogContext" ], "MinimumLevel": { "Default": "Debug", "Override": { "Microsoft": "Warning", "System": "Warning" } } }, } 更多配置请查看https://github.com/serilog/serilog-settings-configuration 上述配置会在应用程序根目录的logs文件夹下,每天生成一个命名类似20191129.log的日志文件 最后要修改一下Program.cs,代替默认的日志组件 public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseConfiguration(new ConfigurationBuilder().SetBasePath(Environment.CurrentDirectory).AddJsonFile("appsettings.json").Build()); webBuilder.UseStartup(); webBuilder.UseSerilog((whbContext, configureLogger) => { configureLogger.ReadFrom.Configuration(whbContext.Configuration); }); }); 文件分块上传 文件上传就像登录验证一样常用,哪个应用还不上传个头像啥的,所以我也打算整合到模板项目中,如果是单纯的上传也就没必要说了,这里主要说的是一种大文件上传的解决方法: 分块上传。 分块上传是需要客户端配合的,客户端把一个大文件分好块,一小块一小块的上传,上传完成之后服务端按照顺序合并到一起就是整个文件了。 所以我们先定义分块上传的参数: string identifier : 文件标识,一个文件的唯一标识, int chunkNumber :当前块所以,我是从1开始的 int chunkSize :每块大小,客户端设置的固定值,单位为byte,一般2M左右就可以了 long totalSize:文件总大小,单位为byte int totalChunks:总块数 这些参数都好理解,在服务端验证和合并文件时需要。 开始的时候我是这样处理的,客户端每上传一块,我会把这块的内容写到一个临时文件中,使用identifier和chunkNumber来命名,这样就知道是哪个文件的哪一块了,当上传完最后一块之后,也就是chunkNumber==totalChunks的时候,我将所有的分块小文件合并到目标文件,然后返回url。 这个逻辑是没什么问题,只需要一个机制保证合并文件的时候所有块都已上传就可以了,为什么要这样一个机制呢,主要是因为客户端的上传可能是多线程的,而且也不能完全保证http的响应顺序和请求顺序是一样的,所以虽然上传完最后一块才会合并,但是还是需要一个机制判断一下是否所有块都上传完毕,没有上传完还要等待一下(想一想怎么实现!)。 后来在实际上传过程中发现最后一块响应会比较慢,特别是文件很大的时候,这个也好理解,因为最后一块上传会合并文件,所以需要优化一下。 这里就使用到了队列的概念了,我们可以把每次上传的内容都放在队列中,然后使用另一个线程从队列中读取并写入目标文件。在这个场景中BlockingCollection是最合适不过的了。 我们定义一个实体类,用于保存入列的数据: public class UploadChunkItem { public byte[] Data { get; set; } public int ChunkNumber { get; set; } public int ChunkSize { get; set; } public string FilePath { get; set; } } 然后定义一个队列写入器 public class UploadChunkWriter { public static UploadChunkWriter Instance = new UploadChunkWriter(); private BlockingCollection _queue; private int _writeWorkerCount = 3; private Thread _writeThread; public UploadChunkWriter() { _queue = new BlockingCollection(500); _writeThread = new Thread(this.Write); } public void Write() { while (true) { //单线程写入 //var item = _queue.Take(); //using (var fileStream = File.Open(item.FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite)) //{ // fileStream.Position = (item.ChunkNumber - 1) * item.ChunkSize; // fileStream.Write(item.Data, 0, item.Data.Length); // item.Data = null; //} //多线程写入 Task[] tasks