二、重要文件说明
1、Program.cs
namespace CoreBackend.Api
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
这个Program是程序的入口, 看起来很眼熟, 是因为asp.net core application实际就是控制台程序(console application).
它是一个调用asp.net core 相关库的console application.
Main方法里面的内容主要是用来配置和运行程序的.
因为我们的web程序需要一个宿主, 所以 BuildWebHost这个方法就创建了一个WebHostBuilder. 而且我们还需要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作为reverse proxy还是有很多有优点的: 例如,IIS可以过滤请求, 管理证书, 程序崩溃时自动重启等.
UseStartup<Startup>(), 这句话表示在程序启动的时候, 我们会调用Startup这个类.
Build()完之后返回一个实现了IWebHost接口的实例(WebHostBuilder), 然后调用Run()就会运行Web程序, 并且阻止这个调用的线程, 直到程序关闭.
如果想要对AspNetCore源码进行研究,可以查看源码,这里提供两个方法:
1、F12,当然这个不能看到详细的,你需要安装一个组件,VS2017 Resharper
2、查看Github 开源源码 https://github.com/aspnet/MetaPackages/tree/master/src/Microsoft.AspNetCore
2、Startup.cs
namespace CoreBackend.Api
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//判断是否是环境变量
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//这个就是一个简单的中间件写法
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}
其实Startup算是程序真正的切入点.
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.UseMvc()这个方法就行. 但是目前, 所有的http请求都会导致返回"Hello World!".
看一看我们项目的最后,Configure方法是如何配置的:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// 在开发环境中,使用异常页面,这样可以暴露错误堆栈信息,所以不要放在生产环境。
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// 在非开发环境中,使用HTTP严格安全传输(or HSTS) 对于保护web安全是非常重要的。
// 强制实施 HTTPS 在 ASP.NET Core,配合 app.UseHttpsRedirection
//app.UseHsts();
}
#region Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
//之前是写死的
//c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1");
//c.RoutePrefix = "";//路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件,注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉
//根据版本名称倒序 遍历展示
typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version =>
{
// 注意这个 ApiName 和 要和上边 ConfigureServices 中配置swagger的name要大小写一致,具体查看我的blog.core源码
c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}");
});
});
#endregion
#region Authen
//app.UseMiddleware<JwtTokenAuth>();//注意此授权方法已经放弃,请使用下边的官方验证方法。但是如果你还想传User的全局变量,还是可以继续使用中间件
app.UseAuthentication();
#endregion
#region CORS
//跨域第二种方法,使用策略,详细策略信息在ConfigureService中
app.UseCors("LimitRequests");//将 CORS 中间件添加到 web 应用程序管线中, 以允许跨域请求。
//跨域第一种版本,请要ConfigureService中配置服务 services.AddCors();
// app.UseCors(options => options.WithOrigins("http://localhost:8021").AllowAnyHeader()
//.AllowAnyMethod());
#endregion
// 跳转https
app.UseHttpsRedirection();
// 使用静态文件
app.UseStaticFiles();
// 使用cookie
app.UseCookiePolicy();
// 返回错误码
app.UseStatusCodePages();//把错误码返回前台,比如是404
app.UseMvc();
}
3、调试方法
.net core 调试的两种方法
1、通过IIS调试
2、项目自带的Kestrel web应用调式
三、注册并使用MVC
因为asp.net core 2.0使用了一个大而全的metapackage, 所以这些基本的services和middleware是不需要另外安装的.
首先, 在ConfigureServices里面向Container注册MVC: services.AddMvc();
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(); // 注册MVC到Container
}
然后再Configure里面告诉程序使用mvc中间件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
注意顺序, 应该在处理异常的middleware后边调用app.UseMvc(), 所以处理异常的middleware可以在把request交给mvc之间就处理异常, 更重要的是它还可以捕获并处理返回MVC相关代码执行中的异常.
然后别忘了把app.Run那部分代码去掉. 然后改回到Develpment环境, 跑一下, 试试效果:
Chrome显示了一个空白页, 按F12, 显示了404 Not Found错误.
这是因为我只添加了MVC middleware, 但是它啥也没做, 也没有找到任何可用于处理请求的代码, 所以我们要添加Controller来返回数据/资源等等
四、核心知识点
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前缀.
以下不是本系列,就看思路即可,不用敲代码
//以下不是本系列教程,就看思路即可,不用敲代码
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
以下不是本系列,就看思路即可,不用敲代码
//以下不是本系列教程,就看思路即可,不用敲代码
[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进行:
//以下不是本系列教程,就看思路即可,不用敲代码
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里面添加验证逻辑:
//以下不是本系列教程,就看思路即可,不用敲代码
[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
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的方法:
//以下不是本系列教程,就看思路即可,不用敲代码
[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作为参数一起传到前台.