前言
至于为什么要搭建.Net Core 平台,这个网上的解释以及铺天盖地,想了想,还是感觉重要的一点,跨平台,嗯!没错,而且比.Net 更容易搭建,速度也更快,所有的包均由Nuget提供,不再像以前的单纯引入组件,
已经没有了之前的Assemblies和COM的引入,初次使用感觉会很别扭,不过使用多了,发现还是很方便的,所以你一定要会使用Nuget,真的很强大,这点儿设计思路感觉更像Linux了。
下边这三点,是先对 .net core 有一个初步的认识,看得懂或者看不懂都没有关系,以后大家肯定都会明白的:
1、.net core 框架性能测试
http://www.techempower.com/benchmarks/ 我们可以通过这个web框架性能测试来看看 aspcore 的性能
2、.net core 执行过程
3、中间件执行过程
启动的时候先执行该中间件类的构造函数,然后一路 Next() ;下去,返回的时候,正好是反向的,执行的是该类的逻辑部分:
4、AOP切面
5、整体框架结构与数据库表UML
一、创建第一个Core
说了从零开始,就得从零开始,老生常谈,开始。
1、SDK 安装
当然,前提是你得安装.Net Core 的 SDK 环境,这里只能使用 vs 2019 且版本在 16.3 以上。
下载 SDK 地址 :https://dotnet.microsoft.com/download
选择指定的平台即可安装:
代码语言:javascript复制这里说下,SDK 和 RunTime 的区别:
1、SDK 是用来开发 NetCore 的,内部捆绑了 Runtime 运行时;
2、但是如果只想运行 NetCore 项目的话,只需要在服务器中安装 Runtime 运行时即可;
怎么判断安装成功了呢?直接运行命令,如果有结果证明成功了:
2、新建项目
1、打开vs2019 ,创建一个新的项目,这里有很多模板,大家都可以试试:
2、点击下一步 Next。
3、然后创建模板:
这里要注意下,关于Https选项问题,有很多小伙伴在以后的接口调用中,勾选了这个,但是还是一直使用 http 协议去访问,导致找不到响应的接口地址。
1、是你的项目创建的时候,勾选了 Https 选项,如果你还没有创建,那就可以不要勾选那个 HTTPS选项。 2、如果你的项目已经创建好了,每次访问都是HTTPS的,但是你不想这么做,可以在 launthSettings.json 文件中,把sslPort 端口号改成0即可
3、删除IIS配置,默认使用 kestrel 服务器
3、项目整体结构分析
接下来咱们看看这个项目都包含了哪些东西:
这里默认,有一个 WeatherForecastController.cs ,打开后: 1、当前控制器继承了 ControllerBase 基类; 2、有一个路由特性,可以配置当前路由规则,比如当前的是[Route("[controller]")],表示 Url 路径为 :localhost:5000/weacherforecast 如果你配置 [Route("[controller]/[action]")] 表明 Url 路径为 :localhost:5000/weacherforecast/get 3、发现有一个默认的构造函数,里边已经注入了日志 ILogger,这个依赖注入相关内容,以后会讲到; 4、默认提供了一个 get 方法,随机生成了一个天气集合;
接下来点开appsettings.json文件,这里就是整个系统app的配置地址,更类似以前的web.config,以后大家会用到。
继续往下,打开Startup.cs 文件这里是整个项目的启动文件,所有的启动相关的都会在这里配置,比如 依赖注入,跨域请求,Redis缓存等,更多详情在以后的文章中都会有所提起
二、重要文件说明
1、Program.cs
代码语言:javascript复制namespace blog.core.test3._0
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
下边的内容,如果不理解,也无所谓,以后会慢慢懂的,至少要读几遍,知道相关概念。
这个Program是程序的入口, 看起来很眼熟, 是因为asp.net core application实际就是控制台程序(console application).
它是一个调用asp.net core 相关库的console application.
Main方法里面的内容主要是用来配置和运行程序的。
因为我们的web程序需要一个宿主,所以CreateHostBuilder这个方法就创建了一个IHostBuilder. 而且我们还需要Web Server.
asp.net core 自带了两种http servers, 一个是WebListener, 它只能用于windows系统, 另一个是kestrel, 它是跨平台的.
kestrel是默认的web server, 就是通过UseKestrel()这个方法来启用的.
但是我们开发的时候使用的是IIS Express, 调用UseIISIntegration()这个方法是启用IIS Express, 它作为Kestrel的Reverse Proxy server来用.
如果在windows服务器上部署的话, 就应该使用IIS作为Kestrel的反向代理服务器来管理和代理请求.
如果在linux上的话, 可以使用apache, nginx等等的作为kestrel的proxy server.
当然也可以单独使用kestrel作为web 服务器, 但是使用iis作为反向代理还是有很多有优点的: 例如,IIS可以过滤请求, 管理证书, 程序崩溃时自动重启等.
不过目前因为跨平台,所以使用较多的还是 nginx。
webBuilder.UseStartup<Startup>();, 这句话表示在程序启动的时候, 我们会调用Startup这个类.
Build()完之后返回一个实现了 IHost 接口的实例(IHostBuilder), 然后调用Run()就会运行Web程序, 并且阻止这个调用的线程, 直到程序关闭.
如果想要对AspNetCore源码进行研究,可以查看源码,这里提供两个方法:
1、F12,当然这个不能看到详细的,你需要安装一个组件,VS2017 Resharper
2、查看Github 开源源码 https://github.com/aspnet/MetaPackages/tree/master/src/Microsoft.AspNetCore
最后让大家看看我的 Blog.Core 项目最终的 Services :
2、Startup.cs
代码语言:javascript复制 public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
其实Startup算是程序真正的切入点,是配置服务和中间件的启动类。
Startup 默认构造函数,注入了配置项 IConfiguration。
ConfigureServices方法是用来把services(各种服务, 例如identity, ef, mvc等等包括第三方的, 或者自己写的)加入(register)到container(asp.net core的容器)中去, 并配置这些services. 这个container是用来进行dependency injection的(依赖注入). 所有注入的services(此外还包括一些框架已经注册好的services) 在以后写代码的时候, 都可以将它们注入(inject)进去. 例如上面的Configure方法的参数, app, env, loggerFactory都是注入进去的services.
Configure 方法是asp.net core程序用来具体指定如何处理每个http请求的, 例如我们可以让这个程序知道我使用mvc来处理http请求, 那就调用 app.UseEndpoints 这个方法就行,这个是一个短路中间件,表示 http 请求到了这里就不往下走了.
看一看我们项目的最后,Configure方法是如何配置的,这个现在看看就行,以后肯定会让大家都慢慢掌握:
代码语言:javascript复制 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBlogArticleServices _blogArticleServices)
{
#region ReuestResponseLog
if (Appsettings.app("AppSettings", "Middleware_RequestResponse", "Enabled").ObjToBool())
{
app.UseReuestResponseLog();//记录请求与返回数据
}
#endregion
#region Environment
if (env.IsDevelopment())
{
// 在开发环境中,使用异常页面,这样可以暴露错误堆栈信息,所以不要放在生产环境。
app.UseDeveloperExceptionPage();
//app.Use(async (context, next) =>
//{
// //这里会多次调用,这里测试一下就行,不要打开注释
// //var blogs =await _blogArticleServices.GetBlogs();
// var processName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
// Console.WriteLine(processName);
// await next();
//});
}
else
{
app.UseExceptionHandler("/Error");
// 在非开发环境中,使用HTTP严格安全传输(or HSTS) 对于保护web安全是非常重要的。
// 强制实施 HTTPS 在 ASP.NET Core,配合 app.UseHttpsRedirection
//app.UseHsts();
}
#endregion
#region Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
//根据版本名称倒序 遍历展示
typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version =>
{
c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}");
});
// 将swagger首页,设置成我们自定义的页面,记得这个字符串的写法:解决方案名.index.html
c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.index.html");//这里是配合MiniProfiler进行性能监控的,《文章:完美基于AOP的接口性能分析》,如果你不需要,可以暂时先注释掉,不影响大局。
c.RoutePrefix = ""; //路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件,注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉,如果你想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "doc";
});
#endregion
#region MiniProfiler
app.UseMiniProfiler();
#endregion
#region CORS
//跨域第二种方法,使用策略,详细策略信息在ConfigureService中
app.UseCors("LimitRequests");//将 CORS 中间件添加到 web 应用程序管线中, 以允许跨域请求。
#region 跨域第一种版本
//跨域第一种版本,请要ConfigureService中配置服务 services.AddCors();
// app.UseCors(options => options.WithOrigins("http://localhost:8021").AllowAnyHeader()
//.AllowAnyMethod());
#endregion
#endregion
// 跳转https
//app.UseHttpsRedirection();
// 使用静态文件
app.UseStaticFiles();
// 使用cookie
app.UseCookiePolicy();
// 返回错误码
app.UseStatusCodePages();//把错误码返回前台,比如是404
app.UseRouting();
#region 第三步:开启认证中间件
//此授权认证方法已经放弃,请使用下边的官方验证方法。但是如果你还想传User的全局变量,还是可以继续使用中间件,第二种写法//app.UseMiddleware<JwtTokenAuth>();
//app.UseJwtTokenAuth();
//如果你想使用官方认证,必须在上边ConfigureService 中,配置JWT的认证服务 (.AddAuthentication 和 .AddJwtBearer 二者缺一不可)
app.UseAuthentication();
#endregion
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapHub<ChatHub>("/api2/chatHub");
});
}
3、运行方法
.net core 调试的两种方法
1、通过IIS调试
2、项目自带的Kestrel web应用调式
三、注册并使用MVC
首先,在 ConfigureServices里添加控制器服务,因为我们这里是api项目,所以只需要添加 AddController() 即可,如果是 MVC,可以使用 AddControllersWithViews();
然后在 Configure 中,配置一下中间件:
代码语言:javascript复制 app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
这样就达到了目的,这个不仅仅是 mvc 的开发过程,其他的也都是这样的,以后我们开发其他的,都需要按照这个过程开发即可。
四、核心知识点
1、Routing 路由
路由有两种方式: Convention-based (按约定), attribute-based(基于路由属性配置的).
其中convention-based (基于约定的) 主要用于MVC (返回View或者Razor Page那种的).
Web api 推荐使用attribute-based.
这种基于属性配置的路由可以配置Controller或者Action级别, uri会根据Http method然后被匹配到一个controller里具体的action上.
常用的Http Method有:
- Get, 查询, Attribute: HttpGet, 例如: '/api/product', '/api/product/1'
- POST, 创建, HttpPost, '/api/product'
- PUT 整体修改更新 HttpPut, '/api/product/1'
- PATCH 部分更新, HttpPatch, '/api/product/1'
- DELETE 删除, HttpDelete, '/api/product/1
还有一个Route属性(attribute)也可以用于Controller层, 它可以控制action级的URI前缀.
代码语言:javascript复制//以下不是本系列教程,就看思路即可,不用敲代码
namespace CoreBackend.Api.Controllers
{
//[Route("api/product")]
[Route("api/[controller]")]
public class ProductController: Controller
{
[HttpGet]
public JsonResult GetProducts()
{
return new JsonResult(new List<Product>
{
new Product
{
Id = 1,
Name = "牛奶",
Price = 2.5f
},
new Product
{
Id = 2,
Name = "面包",
Price = 4.5f
}
});
}
}
}
使用[Route("api/[controller]")], 它使得整个Controller下面所有action的uri前缀变成了"/api/product", 其中[controller]表示XxxController.cs中的Xxx(其实是小写).
也可以具体指定, [Route("api/product")], 这样做的好处是, 如果ProductController重构以后改名了, 只要不改Route里面的内容, 那么请求的地址不会发生变化.
然后在GetProducts方法上面, 写上HttpGet, 也可以写HttpGet(). 它里面还可以加参数,例如: HttpGet("all"), 那么这个Action的请求的地址就变成了 "/api/product/All".
2、内容协商 Content Negotiation
如果 web api提供了多种内容格式, 那么可以通过Accept Header来选择最好的内容返回格式: 例如:
application/json, application/xml等等
如果设定的格式在web api里面没有, 那么web api就会使用默认的格式.
asp.net core 默认提供的是json格式, 也可以配置xml等格式.
目前只考虑 Output formatter, 就是返回的内容格式.
如果想输出xml格式,就配置这里:
3、创建Post Action
以下不是本系列,就看思路即可,不用敲代码
代码语言:javascript复制//以下不是本系列教程,就看思路即可,不用敲代码
[Route("{id}", Name = "GetProduct")]
public IActionResult GetProduct(int id)
{
var product = ProductService...(x => x.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
[HttpPost]
public IActionResult Post([FromBody] ProductCreation product)
{
if (product == null)
{
return BadRequest();
}
var maxId = ProductService.Max(x => x.Id);
var newProduct = new Product
{
Id = maxId,
Name = product.Name,
Price = product.Price
};
ProductService.Add(newProduct);
return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct);
}
[HttpPost] 表示请求的谓词是Post. 加上Controller的Route前缀, 那么访问这个Action的地址就应该是: 'api/product'
后边也可以跟着自定义的路由地址, 例如 [HttpPost("create")], 那么这个Action的路由地址就应该是: 'api/product/create'.
[FromBody] , 请求的body里面包含着方法需要的实体数据, 方法需要把这个数据Deserialize成ProductCreation, [FromBody]就是干这些活的.
客户端程序可能会发起一个Bad的Request, 导致数据不能被Deserialize, 这时候参数product就会变成null. 所以这是一个客户端发生的错误, 程序为让客户端知道是它引起了错误, 就应该返回一个Bad Request 400 (Bad Request表示客户端引起的错误)的 Status Code.
传递进来的model类型是 ProductCreation, 而我们最终操作的类型是Product, 所以需要进行一个Map操作, 目前还是挨个属性写代码进行Map吧, 以后会改成Automapper.
返回 CreatedAtRoute: 对于POST, 建议的返回Status Code 是 201 (Created), 可以使用CreatedAtRoute这个内置的Helper Method. 它可以返回一个带有地址Header的Response, 这个Location Header将会包含一个URI, 通过这个URI可以找到我们新创建的实体数据. 这里就是指之前写的GetProduct(int id)这个方法. 但是这个Action必须有一个路由的名字才可以引用它, 所以在GetProduct方法上的Route这个attribute里面加上Name="GetProduct", 然后在CreatedAtRoute方法第一个参数写上这个名字就可以了, 尽管进行了引用, 但是Post方法走完的时候并不会调用GetProduct方法. CreatedAtRoute第二个参数就是对应着GetProduct的参数列表, 使用匿名类即可, 最后一个参数是我们刚刚创建的数据实体.
运行程序试验一下, 注意需要在Headers里面设置Content-Type: application/json.
4、Validation 验证
针对上面的Post方法, 如果请求没有Body, 参数product就会是null, 这个我们已经判断了; 如果body里面的数据所包含的属性在product中不存在, 那么这个属性就会被忽略.
但是如果body数据的属性有问题, 比如说name没有填写, 或者name太长, 那么在执行action方法的时候就会报错, 这时候框架会自动抛出500异常, 表示是服务器的错误, 这是不对的. 这种错误是由客户端引起的, 所以需要返回400 Bad Request错误.
验证Model/实体, asp.net core 内置可以使用 Data Annotations进行:
代码语言:javascript复制//以下不是本系列教程,就看思路即可,不用敲代码
using System;
using System.ComponentModel.DataAnnotations;
namespace CoreBackend.Api.Dtos
{
public class ProductCreation
{
[Display(Name = "产品名称")]
[Required(ErrorMessage = "{0}是必填项")]
// [MinLength(2, ErrorMessage = "{0}的最小长度是{1}")]
// [MaxLength(10, ErrorMessage = "{0}的长度不可以超过{1}")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
public string Name { get; set; }
[Display(Name = "价格")]
[Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
public float Price { get; set; }
}
}
这些Data Annotation (理解为用于验证的注解), 可以在System.ComponentModel.DataAnnotation找到, 例如[Required]表示必填, [MinLength]表示最小长度, [StringLength]可以同时验证最小和最大长度, [Range]表示数值的范围等等很多.
[Display(Name="xxx")]的用处是, 给属性起一个比较友好的名字.
其他的验证注解都有一个属性叫做 ErrorMessage (string), 表示如果验证失败, 就会把ErrorMessage的内容添加到错误结果里面去. 这个ErrorMessage可以使用参数, {0}表示Display的Name属性, {1}表示当前注解的第一个变量, {2}表示当前注解的第二个变量.
在Controller里面添加验证逻辑:
代码语言:javascript复制//以下不是本系列教程,就看思路即可,不用敲代码
[HttpPost]
public IActionResult Post([FromBody] ProductCreation product)
{
if (product == null)
{
return BadRequest();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var maxId = ProductService.Max(x => x.Id);
var newProduct = new Product
{
Id = maxId,
Name = product.Name,
Price = product.Price
};
ProductService.Add(newProduct);
return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct);
}
ModelState: 是一个Dictionary, 它里面是请求提交到Action的Name和Value的对们, 一个name对应着model的一个属性, 它也包含了一个针对每个提交的属性的错误信息的集合.
每次请求进到Action的时候, 我们在ProductCreationModel添加的那些注解的验证, 就会被检查. 只要其中有一个验证没通过, 那么ModelState.IsValid属性就是False. 可以设置断点查看ModelState里面都有哪些东西.
如果有错误的话, 我们可以把ModelState当作 Bad Request的参数一起返回到前台.
5、PUT请求
put应该用于对model进行完整的更新.
首先最好还是单独为Put写一个Dto Model, 尽管属性可能都是一样的, 但是也建议这样写, 实在不想写也可以.
ProducModification.cs
代码语言:javascript复制 public class ProductModification
{
[Display(Name = "产品名称")]
[Required(ErrorMessage = "{0}是必填项")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
public string Name { get; set; }
[Display(Name = "价格")]
[Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
public float Price { get; set; }
}
然后编写Controller的方法:
代码语言:javascript复制//以下不是本系列教程,就看思路即可,不用敲代码
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] ProductModification product)
{
if (product == null)
{
return BadRequest();
}
if (product.Name == "产品")
{
ModelState.AddModelError("Name", "产品的名称不可以是'产品'二字");
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var model = ProductService.SingleOrDefault(x => x.Id == id);
if (model == null)
{
return NotFound();
}
model.Name = product.Name;
model.Price = product.Price;
// return Ok(model);
return NoContent();
}
按照Http Put的约定, 需要一个id这样的参数, 用于查找现有的model.
由于Put做的是完整的更新, 所以把ProducModification整个Model作为参数.
进来之后, 进行了一套和POST一模一样的验证, 这地方肯定可以改进, 如果验证逻辑比较复杂的话, 到处写同样验证逻辑肯定是不好的, 所以建议使用FluentValidation.
然后, 把ProductModification的属性都映射查询找到给Product, 这个以后用AutoMapper来映射.
返回: PUT建议返回NoContent(), 因为更新是客户端发起的, 客户端已经有了最新的值, 无须服务器再给它传递一次, 当然了, 如果有些值是在后台更新的, 那么也可以使用Ok(xxx)然后把更新后的model作为参数一起传到前台.
五、结语
好啦,项目搭建就这么愉快的解决了,而且你也应该简单了解了.Net Core API是如何安装,创建,各个文件的意义以及如何运作,如何配置等,但是既然是接口,那一定是要前后端一起进行配置,使用,交流的平台,从上文看出,每次都特别麻烦,而且不直观,UI 不友好,怎么办呢?
下一节我们就使用一个神器 Swagger,一个快速,轻量级的项目RESTFUL接口的文档在线自动生成 功能测试功能软件。
Github && Gitee
https://github.com/anjoy8/Blog.Core.git
NOTE
如何不会使用Git,可以参考
https://www.jianshu.com/p/2b666a08a3b5