【 .NET Core 3.0 】框架之二 || 后端项目搭建


前言


至于为什么要搭建.Net Core 平台,这个网上的解释以及铺天盖地,想了想,还是感觉重要的一点,跨平台,嗯!没错,而且比.Net 更容易搭建,速度也更快,所有的包均由Nuget提供,不再像以前的单纯引入组件,

640?wx_fmt=png

已经没有了之前的Assemblies和COM的引入,初次使用感觉会很别扭,不过使用多了,发现还是很方便的,所以你一定要会使用Nuget,真的很强大,这点儿设计思路感觉更像Linux了。

下边这三点,是先对 .net core  有一个初步的认识,看得懂或者看不懂都没有关系,以后大家肯定都会明白的:

 

1、.net core 框架性能测试

http://www.techempower.com/benchmarks/ 我们可以通过这个web框架性能测试来看看 aspcore 的性能

640?wx_fmt=png

640?wx_fmt=png

 

2、.net core 执行过程

640?wx_fmt=png

3、中间件执行过程

启动的时候先执行该中间件类的构造函数,然后一路 Next() ;下去,返回的时候,正好是反向的,执行的是该类的逻辑部分:

640?wx_fmt=png

 

4、AOP切面

640?wx_fmt=png

 

640?wx_fmt=png

 

 

5、整体框架结构与数据库表UML

 640?wx_fmt=png

 

 

640?wx_fmt=png

 

 

一、创建第一个Core

    说了从零开始,就得从零开始,老生常谈,开始。

1、SDK 安装

当然,前提是你得安装.Net Core 的 SDK 环境,这里只能使用 vs 2019 且版本在 16.3 以上。

 

下载 SDK 地址 :https://dotnet.microsoft.com/download


选择指定的平台即可安装:

640?wx_fmt=png

 

这里说下,SDK 和 RunTime 的区别:
1、SDK 是用来开发 NetCore 的,内部捆绑了 Runtime 运行时;
2、但是如果只想运行 NetCore 项目的话,只需要在服务器中安装 Runtime 运行时即可;

 

怎么判断安装成功了呢?直接运行命令,如果有结果证明成功了:

 

640?wx_fmt=png

 

 

2、新建项目

1、打开vs2019 ,创建一个新的项目,这里有很多模板,大家都可以试试:

640?wx_fmt=png

 

 

2、点击下一步 Next。



3、然后创建模板:

640?wx_fmt=png

 

这里要注意下,关于Https选项问题,有很多小伙伴在以后的接口调用中,勾选了这个,但是还是一直使用 http 协议去访问,导致找不到响应的接口地址。

640?wx_fmt=png


 

1、是你的项目创建的时候,勾选了 Https 选项,如果你还没有创建,那就可以不要勾选那个 HTTPS选项。

2、如果你的项目已经创建好了,每次访问都是HTTPS的,但是你不想这么做,可以在 launthSettings.json 文件中,把sslPort 端口号改成0即可
640?wx_fmt=png

 

3、删除IIS配置,默认使用 kestrel 服务器


 

640?wx_fmt=png


 

 

3、项目整体结构分析

接下来咱们看看这个项目都包含了哪些东西: 

640?wx_fmt=png

 

这里默认,有一个 WeatherForecastController.cs ,打开后:

1、当前控制器继承了 ControllerBase 基类;


2、有一个路由特性,可以配置当前路由规则,比如当前的是[Route("[controller]")],表示 Url 路径为 :localhost:5000/weacherforecast

   如果你配置 [Route("[controller]/[action]")] 表明 Url 路径为 :localhost:5000/weacherforecast/get


3、发现有一个默认的构造函数,里边已经注入了日志 ILogger,这个依赖注入相关内容,以后会讲到;

4、默认提供了一个 get 方法,随机生成了一个天气集合;


640?wx_fmt=png




640?wx_fmt=png

 

640?wx_fmt=png


接下来点开appsettings.json文件,这里就是整个系统app的配置地址,更类似以前的web.config,以后大家会用到。


 

 

继续往下,打开Startup.cs 文件这里是整个项目的启动文件,所有的启动相关的都会在这里配置,比如 依赖注入,跨域请求,Redis缓存等,更多详情在以后的文章中都会有所提起

 

640?wx_fmt=png


二、重要文件说明

1、Program.cs

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来用.

640?wx_fmt=png

 

 

如果在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 

640?wx_fmt=png

 

最后让大家看看我的 Blog.Core 项目最终的 Services :

640?wx_fmt=png




2、Startup.cs

    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.

640?wx_fmt=png

 

Configure 方法是asp.net core程序用来具体指定如何处理每个http请求的, 例如我们可以让这个程序知道我使用mvc来处理http请求, 那就调用 app.UseEndpoints 这个方法就行,这个是一个短路中间件,表示 http 请求到了这里就不往下走了. 

640?wx_fmt=png

 

 

看一看我们项目的最后,Configure方法是如何配置的,这个现在看看就行,以后肯定会让大家都慢慢掌握:



 

 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调试

640?wx_fmt=png

 

2、项目自带的Kestrel web应用调式

640?wx_fmt=png

 

 

 

三、注册并使用MVC


首先,在 ConfigureServices里添加控制器服务,因为我们这里是api项目,所以只需要添加 AddController() 即可,如果是 MVC,可以使用 AddControllersWithViews();


然后在 Configure 中,配置一下中间件:

 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前缀.

//以下不是本系列教程,就看思路即可,不用敲代码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格式,就配置这里:

 640?wx_fmt=png

 

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作为参数一起传到前台.

 

 

 


五、结语

    好啦,项目搭建就这么愉快的解决了,而且你也应该简单了解了.Net Core API是如何安装,创建,各个文件的意义以及如何运作,如何配置等,但是既然是接口,那一定是要前后端一起进行配置,使用,交流的平台,从上文看出,每次都特别麻烦,而且不直观,UI 不友好,怎么办呢?

    下一节我们就使用一个神器 Swagger,一个快速,轻量级的项目RESTFUL接口的文档在线自动生成+功能测试功能软件。

 

Github && Gitee

https://github.com/anjoy8/Blog.Core.git

 

NOTE

如何不会使用Git,可以参考

https://www.jianshu.com/p/2b666a08a3b5

 

一起学习,一起进步   QQ群:867095512

640?wx_fmt=jpeg


  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值