2、重要文件说明
一、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源码进行研究,能够查看源码,这里提供两个方法:
一、F12,固然这个不能看到详细的,你须要安装一个组件,VS2017 Resharper
二、查看Github 开源源码 https://github.com/aspnet/MetaPackages/tree/master/src/Microsoft.AspNetCore
二、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 =>
{
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();
}
三、调试方法
.net core 调试的两种方法
一、经过IIS调试
二、项目自带的Kestrel web应用调式
3、注册并使用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来返回数据/资源等等
4、核心知识点
一、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".
二、内容协商 Content Negotiation
若是 web api提供了多种内容格式, 那么能够经过Accept Header来选择最好的内容返回格式: 例如:
application/json, application/xml等等
若是设定的格式在web api里面没有, 那么web api就会使用默认的格式.
asp.net core 默认提供的是json格式, 也能够配置xml等格式.
目前只考虑 Output formatter, 就是返回的内容格式.
若是想输出xml格式,就配置这里:
三、建立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.
四、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的参数一块儿返回到前台.
五、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做为参数一块儿传到前台.