用ASP.NET Core 2.0 创建规范的 REST API -- 预备知识 (2) + 准备项目

上一部分预备知识在这 http://www.cnblogs.com/cgzl/p/9010978.htmlhtml

若是您对ASP.NET Core很了解的话,能够不看本文, 本文基本都是官方文档的内容。数据库

ASP.NET Core 预备知识

项目配置

假设在项目的根目录有这样一个json文件, 在ASP.NET Core项目里咱们可使用IConfigurationRoot来使用该json文件做为配置文件, 而IConfigurationRoot是使用ConfigurationBuilder来建立的:json

能够看到ConfigurationBuilder加载了firstConfig.json文件, 使用的是AddJsonFile这个扩展方法. 调用builder的Build方法会获得一个IConfigurationRoot的实例, 它实现了IConfiguration接口, 随后咱们即可以经过遍历它的键值对.windows

其中json文件里的结构数据都最为键值对被扁平化到IConfiguration里了, 咱们能够经过它的key找到对应的值:服务器

像childkey1这种带层次结构的值可使用冒号 做为层次分隔符.mvc

配置文件总会包含这种多层结构的, 更好的办法是把相似的配置进行分组获取, 可使用IConfiguration的GetSection()方法来获取局部的配置:app

 

当有多个配置文件的时候, 配置数据的加载和它们在程序中指定的顺序是同样的, 若是多个文件都有同一个键的话, 那么最后加载的值将会覆盖先前加载的值.less

下面是另外一个配置文件:异步

在firstConfig后加载secondConfig:post

最后key1的值是后加载的secondConfig里面的值.

固然了, 若是firstConfig里面有而secondConfig却没有的键, 它的值确定来自firstConfig.

 

配置提供商

配置数据能够来自多种数据源, 它们多是不一样格式的.

ASP.NET Core 默认支持从下列方式得到配置:

  • 文件格式(INI, JSON, XML)
  • 命令行参数
  • 环境变量
  • 内存中的.NET对象
  • 未加密的Secret管理存储
  • 加密的用户存储, 例如Azure秘钥库
  • 自定义的提供商

这些东西仍是看官方文档吧, 本文使用JSON格式的就够用了.

 

强类型的配置

ASP.NET Core容许把配置数据映射到一个对象类上面.

针对上面的firstConfig.json文件, 咱们建立如下这个类:

而后调用IConfiguration的Bind扩展方法来把键值对集合对值映射到这个强类型对POCO实例里:

 

在标准的ASP.NET Core 2.0的项目模版里, 加载配置文件的步骤被封装了, 默认或加载appSettings.json 以及 appSettings.{环境}.json.

我记得是封装在这里了:

我把firstConfig.json更名为appSettings.json.

而后在Startup里面能够得到IConfiguration:

从打印结果能够看到, 加载的不仅是appSettings里面的内容, 还有系统环境变量的值.

这种状况下, 使用IServiceCollectionConfigure扩展方法能够把配置映射到指定的类上面:

同时这也容许在程序的任何地方注入IOptions<FirstConfig>了:

这个Configure方法不只仅能够映射ConfigurationRoot, 还能够映射配置的一部分:

 

配置变化

在项目运行的时候, 项目的配置信息可能会发生变化.

当采用的是基于文件的配置时, 若是配置数据有变化了, 咱们应该让配置模型从新加载, 这就须要把AddJsonFile里面的配置属性 ReloadOnChange 设置为 true:

这时, 不管在哪各地方使用了IConfigurationRoot和IConfiguration, 它们都会反映出最新的值, 可是IOptions<T>却不行. 即便文件变化了而且配置模型也经过文件提供商进行了更新, IOptions<T>的实例仍然包含的是原始值.

为了让配置数据能够在这种强类型映射的类上体现, 就须要使用IOptionsSnapshot<T>:


IOptionsSnapshot<T> 的开销很小, 能够放心使用

 

日志 

ASP.NET Core 提供了6个内置的日志提供商。

须要使用日志的话,只需注入一个ILogger对象便可,不过该对象首先要在DI容器中注册。

这个ILogger接口主要是提供了Log方法:

记录Log的时候使用Log方法便可:

不过能够看到,该方法参数不少,用起来仍是略显麻烦的。

幸运的是,针对Log还有几个扩展方法,他们就简单了不少:

  • LogCritical,用来记录严重的事情
  • LogDebug,记录调试信息
  • LogError,记录异常
  • LogInformation,记录信息性的事情
  • LogTrace,记录追踪信息
  • LogWarning,记录警告信息

 

在项目中配置和使用Log,只需在Program.cs里调用IWebHostBuilder的ConfigureLogging扩展方法便可:

本例中,咱们把log配置成在控制台输出。

若是只是输出到控制台,其实咱们就画蛇添足了,由于CreateDefaultBuilder这个方法里已经作了一些Log的配置,看一下反编译的源码:

能够看到logging的一些配置数据是从总体配置的Logging部分取出来的,而后配置了使用输出到控制台和Debug窗口的提供商。

记录Log的时候,一般状况下使用那几个扩展方法就足够了:

请注意,这里我注入的是ILogger<T>类型的logger,其中T能够用来表示日志的分类,它能够是任何类型,但一般是记录日志时所在的类。

运行项目后,能够看到我记录的日志:

 

一样也能够在一个类里面把记录的日志分为不一样的分类,这时候你可使用ILoggerFactory,这样就能够随时建立logger了,并把它绑定到特定的区域:

不知道您有没有发现上面这几个例子中日志输出的时候都有个数字 [0], 它是事件的标识符。由于上面的例子中咱们没有指定事件的ID,因此就取默认值0。使用事件ID仍是能够帮助咱们区分和关联记录的日志的。

 

每次写日志的时候, 都须要经过不一样的方式指明LogLevel, LogLevel代表的是严重性.

下面是ASP.NET Core里面定义的LogLevel(它是个枚举), 按严重性从低到高排序的:

Trace = 0, 它能够包含敏感拘束, 默认在生产环境中它是被禁用掉的.

Debug = 1, 也是在调试使用, 应该在生产环境中禁用, 可是遇到问题须要调试能够临时启用.

Information = 2, 用来追踪应用程序的整体流程.

Warning = 3, 一般用于记录非正常或意外的事件, 也能够包括不会致使应用程序中止的错误和其余事件, 例如验证错误等.

Error = 4, 用于记录没法处理的错误和异常, 这些信息意味着当前的活动或操做发生了错误, 但不是应用程序级别的错误.

Critical = 5, 用于记录须要当即处理的事件, 例如数据丢失或磁盘空间不足.

None = 6, 若是你不想输出日志, 你能够把程序的最低日志级别设置为None, 此外还能够用来过滤日志.

 

记录的日志信息是能够带参数的, 使用消息模板(也就是消息主题和参数分开), 格式以下:

一样也支持字符串插值:

第二种方式代码的可读性更强一些, 并且它们输出的结果没有什么区别:

可是对于日志系统来讲, 这两种方式是不同的. 经过消息模板的方式(消息和参数分开的方式), 日志提供商能够实现语义日志或叫作结构化日志, 它们能够把参数单独的出入到日志系统里面进行单独存储, 不只仅是格式化的日志信息.

此外, 用重载的方法, 记录日志时也能够包含异常对象.

 

日志分组

咱们可使用相同的日志信息来表示一组操做, 这须要使用scope, scope继承了IDisposable接口, 经过ILogger.BeginScope<TState>能够获得scope:

使用scope, 还有一点须要注意, 须要在日志提供商上把IncludeScopes属性设置为true:

您能够发现, 日志被输出了两遍, 这是由于WebHost.CreateDefaultBuilder方法里面已经配置使用了AddConsole()方法, 我再配置一遍的话就至关于又添加了一个输出到控制台的日志提供商.

因此, 我能够不采用这个构建模式建立IWebHost, 改成直接new一个:

这样就正确了. 能够看到日志信息的第一行内容是同样的, 第二行是各自的日志信息.

 

日志的过滤

咱们能够为整个程序设定日志记录的最低级别, 也能够为某个日志提供商和分类指定特定的过滤器.

设置全局最低记录日志的级别使用SetMinimumLevel()扩展方法:

若是想彻底不输出日志的话, 能够把最低记录的级别设为LogLevel.None.

咱们还能够为不一样场景设置不一样的最低记录级别:

而后分别创建这两个分类的logger, 并记录:

查看输出结果, 已经按配置进行了过滤:

这里可使用完整的类名做为分类名:

而后使用ILogger<T>便可:

 

针对上面这个例子, 咱们还可使用配置文件:

相应的, 代码也须要改一下:

输出的效果是同样的.

 

日志提供商

ASP.NET Core 内置了6个日志提供商:

  • Console, 使用logging.AddConsole()来启用.
  • Debug, 使用logging.AddDebug()来启用. 它使用的是System.Diagnostics.Debug的Debug.WriteLine()方法, 因为Debug类的全部成员都是被[Conditional("DEBUG")]修饰过了, 因此没法被构建到Release Build里, 也就是生产环境是没法输出的, 除非你把Debug Build做为部署到生产环境😰.
  • EventSource, 使用logging.AddEventSourceLogger()来启用. 它能够把日志记录到事件追踪器, 它是跨平台的, 在windows上, 会记录到Event Tracing for Windows (ETW)
  • EventLog (仅限Windows), 使用logging.AddEventLog()来启用. 它会记录到Windows Event Log.
  • TraceSource (仅限Windows),, 使用logging.AddTraceSource(sourceSwitchName)来启用. 它容许咱们把日志记录到各类的追踪监听器上, 例如 TextWriterTraceListener
  • Azure App Service, 在本地运行程序的时候, 这个提供商并不会起做用, 部署到Azure App Service的.NET Core程序会自动采用该提供商, .NET Core无须调用logging.AddAzureWebAppDiagnostics();该方法. 它会把日志记录到Azure App Service app的文件系统还会写进Azure Storage帐户的blob storage里. 

第三方日志提供商

第三方的提供商有不少: Serilog, NLog, Elmah.IO, Loggr, JSNLog等等.

 

处理异常

ASP.NET Core 未开发人员提供了一个异常信息页面, 它是运行时生成的, 它封装了异常的各类信息, 例如Stack trace.

 

能够看到只有运行环境是开发时才启用该页面, 上面我抛出了一个异常, 看看访问时会出现什么结果:

 

这就是异常页面, 里面包含异常相关的信息.

注意: 该页面之应该在开发时启用, 由于你不想把这些敏感信息在生产环境中暴露.

 

当发送一个请求后, HTTP机制提供的响应老是带着一个状态码, 这些状态码主要有:

  • 1xx, 用于通知报告.
  • 2xx, 表示响应是成功的, 例如 200 OK, 201 Created, 204 No Content.
  • 3xx, 表示某种重定向, 
  • 4xx, 表示客户端引发的错误, 例如 400 Bad Request, 401 Unauthorized, 404 Not Found
  • 5xx, 表示服务器错误, 例如 500 Internal Server Error.

 

默认状况下, ASP.NET Core 项目不提供状态码的细节信息, 可是经过启用StatusCodePagesMiddleware中间件, 咱们能够启用状态码细节信息:

而后当咱们访问一个不存在的路由时, 就会返回如下信息:

咱们也能够自定义返回的状态码信息:

 

OK, 预备知识先介绍到这, 其它相关的知识在创建API的时候穿插着讲吧.

项目开始模板

很是的简单, 先看一下Program.cs:

咱们使用了WebHost.CreateDefaultBuilder()方法, 这个方法的默认配置大约以下:

采用Kestrel服务器, 使用项目个目录做为内容根目录, 默认首先加载appSettings.json, 而后加载appSettings.{环境}.json. 还加载了一些其它的东西例如环境变量, UserSecrect, 命令行参数. 而后配置Log, 会读取配置数据的Logging部分的数据, 使用控制台Log提供商和Debug窗口Log提供商, 最后设置了默认的服务提供商.

而后我添加了本身的一些配置:

使用IIS做为反向代理服务器, 使用Url地址为http://localhost:5000, 使用Startup做为启动类.

而后看Startup:

主要是注册mvc并使用mvc.

随后创建Controllers文件夹, 而后能够添加一个Controller试试是否好用:

 

可选项目配置

注意, 在使用VS2017启动项目的时候, 上面有不少选项:

为了开发时方便, 我把IISExpress这个去掉, 打开并编辑这个文件:

删掉IISExpress的部分, 而后修改一下applicationUrl:

而后启动选项就只剩下一个了:

 

若是你喜欢使用dotnet cli, 能够为项目添加dotnet watch, 打开并编辑 MyRestful.Api.csproj, 添加这行便可:

而后命令行执行 dotnet watch run 便可, 每次程序文件发生变化, 它都会从新编译运行程序:

 

为项目添加EntityFrameworkCore 2.0

关于EFCore 2.0的知识, 仍是请看官方文档吧, 我也写了一篇很是很是入门级的文章, 仅供参考: http://www.cnblogs.com/cgzl/p/8543772.html

新创建两个.NET Core class library类型的项目:

这几个项目的关系是: MyRestful.Infrastructure 须要引用 MyRestful.Core, MyRestful.Api 须要引用其余两个.

 

 并把它们添加到MyRestful.Api项目的引用里.

而后要为MyRestful.Infrastructure项目添加几个包, 能够经过Nuget或者Package Manager Console或者dotnet cli:

Microsoft.EntityFrameworkCore.SqlServer (我打算使用内存数据库, 因此没安装这个)

Microsoft.EntityFrameworkCore.Tools

 

而后在MyRestful.Infrastructure项目里面创建一个DbContext:

 

再创建一个Domain Model, 由于Model和项目的合约(接口)同样都是项目的核心内容, 因此把Model放在MyRestful.Core项目下:

 

而后把这个Model放到MyContext里面:

在Startup.cs里面注册DbContext, 我使用的是内存数据库:

这里要注意: 因为使用的是内存数据库, 因此迁移等一些配置均可以省略了....

作一些种子数据:

这时须要修改一下Program.cs 来添加种子数据:

 好的, 到如今我写一些临时的代码测试一下MyContext:

直接从数据库中读取Domain Model 而后返回, 看看效果(此次使用的是POSTMAN):

能够看到, MyContext是OK的.

到这里, 就会出现一个问题, Controller的Action方法(也就是Web API吧)应该直接返回Domain Model吗?

你也可能知道答案, 不该该这样作. 由于:

像上面例子中的Country这样的Domain Model对于整个程序来讲是内部实现细节, 咱们确定是不想把内部实现细节暴露给外部的, 由于程序是会变化的, 这样就会对全部依赖于这个内部实现的客户端形成破坏. 因此咱们须要在内部实现外面再加上另一层, 这层里面的类就会做为整个程序的公共合约或公共接口(界面的意思, 不是指C#接口).

能够把这件事想象比喻成组装电脑:

组装电脑机箱里有不少零件: 主板, 硬盘, CPU, 内存.....这就就是内部实现细节, 而用户能看到和用到的是先后面板的接口和按钮, 这就是我所说的电脑机箱的公共合约或公共接口. 更重要的是, 组装电脑的零件可能会更新换代, 也许添加一条内存, 换个固态硬盘.....可是全部的这些变化都不会改变(基本上)机箱先后面板的接口和按钮. 这个概念对于软件程序来讲是同样的, 咱们不想暴露咱们的Domain Model给客户端, 因此咱们须要另一套Model类, 它们要看起来很像咱们的Domain Model, 可是这两种model能够独立的进化和改变.

这类Model会到达程序的边界, 做为Controller的输入, 而后Controller把它们串行化以后再输出. 

用REST的术语来讲, 咱们把客户端请求服务器返回的对象叫作资源(Resources).

因此我会在MyRestful.Api项目里创建一个Resources文件夹, 并建立一个类叫作CountryResource.cs (之前我把它叫ViewModel或Dto, 在这里我叫它Resource, 都是一个意思):

如今来讲, 它的属性和Country是同样的.

 

如今的问题是我要把MyContext查询出来的Country映射成CountryResource, 你能够手动编写映射关系, 可是最好的办法仍是使用AutoMapper库(有两个), 安装到MyRestful.Api项目:

AutoMapper AutoMapper.Extensions.Microsoft.DependencyInjection

而后咱们要作两个映射配置文件, 分别是Domain Model ==> Resource 和 Resource ==> Domain Model:

固然了, 也能够作一个配置文件, 我仍是作一个吧:

而后在Startup里面注册AutoMapper便可:

 

 修改Controller测试下:

结果是OK的:

 

Repository 模式

概念不说了, 你能够把Repository想象成就是一堆Domain Models, 咱们可使用这个模式来封装查询等操做. 例以下面红框里面的查询:

这个查询有可能在整个项目中的多个地方被使用, 在稍微大一点的项目里可能会有不少相似的查询, 而Repository模式就是能够解决这个问题的一种方式. 

因此我在MyRestful.Infrastructure项目里创建Repostitories文件夹并创建CountryRepostsitory类:

这里须要注入MyContext, 暂时只须要一个查询方法.

如今Repository作好了, 为了在Controller里面使用(依赖注入), 咱们须要为它抽取出一个接口, 由于咱们不想让Controller与这些实现紧密的耦合在一块儿, 咱们须要作的是把Controller和接口给耦合到一块儿, 这也就是依赖反转原则(DIP, 也就是SOLID里面的D, 高级别的模块不该该依赖于低级别的模块, 它们都应该依赖于抽象):

此外, 单元测试的时候, 咱们能够用实现了IRepository的假Repository, 由于单元测试的时候最好不要依赖外界的资源, 例如数据库, 文件系统等, 最好只用内存中的数据.

因此先抽取接口:

而后配置DI:

在这里ASP.NET Core 提供了三种模式注册实现给接口, 它们表明着不一样的生命周期:

  • Transient: 每次请求(不是指HTTP Request)都会建立一个新的实例,它比较适合轻量级的无状态的(Stateless)的service。
  • Scope: 每次http请求会建立一个实例。
  • Singleton: 在第一次请求的时候就会建立一个实例,之后也只有这一个实例,或者在ConfigureServices这段代码运行的时候建立惟一一个实例。

因为Repository依赖于DbContext, 而DbContext在ASP.NET Core项目配置里是Scope的, 因此每次HTTP请求的生命周期中只有一个DbContext实例, 因此IRepository就应该是Scope的.

修改Controller, 注入并使用IRepository, 去掉MyContext:

经测试, 结果是同样的, 我就不贴图了.

 

还有一个问题, 由于每次HTTP请求只会存在一个MyContext的实例, 而引用该实例的Repository多是多个. 也就是说会存在这种状况, 某个Controller的Action方法里, 使用了多个不一样的Repository, 分别作了个新增, 修改, 删除等操做, 可是保存的时候仍是须要MyContext来作, 把保存动做放到任何一个Repository里面都是不合理的. 并且我以前讲过应该把Repository看做是Domain Models的集合, 例如list, 而list.Save()也没有什么意义. 因此Controller仍是依赖于MyContext, 由于须要它的Save动做, 仍是须要解耦. 

以前讲的使用Repository和依赖注入解耦的方式很大程度上较少了重复的代码, 而把Controller和EFCore解耦还有另一个好处, 由于我有可能会把EFCore换掉, 去使用Dapper 😂, 由于若是项目比较大, 或者愈来愈大, 有一部分业务可能会须要性能比较好的Micro ORM来代替或者其它存储方式等. 因此引用EFCore的地方越少, 就越容易替换.

这时, 就应该使用Unit Of Work 模式了, 首先我添加一个IUnitOfWork的接口, 我把它放在MyRestful.Core项目的interfaces文件夹下了:

只有一个异步方法SaveAsync(). 而后是它的实现类UnitOfWork:

就是这样, 若是你想要替换掉Entity Framework Core的话, 只须要修改UnitOfWork和Repository, 无须修改IUnitOfWork和IRepository, 由于这些接口是项目的合约, 能够看做是不变的 (因此IRepository也应该放在MyRestful.Core里面, 这个之后再改).

而后注册DI:

修改Controller注入IUnitOfWork试试:

这里我又给Repository添加了一个Add方法用于测试, 结果以下:

好的, 没问题.

 

总体结构调整

差很少了, 让咱们再回顾如下DIP原则(依赖反转): 高级别模块不该该依赖于低级别模块, 它们都应该依赖于抽象. 若是把Repository看做是服务的话, 那么使用服务的模块(Controller)就是高级别模块, 服务(Repository)就是低级别模块. 这个问题咱们已经解决了. 

为何要遵循这个原则? 由于要减小程序变化带来的影响.

看这张图:

就从一个方面来讲, 若是Repository变化或重编译了, 那么Controller颇有可能会变化并确定须要从新编译, 也就是全部依赖于Repository的类都会被从新编译.

而使用DIP原则以后:

咱们能够在Repository里面作出不少更改, 可是这些变化都不会影响到Controller, 由于Controller并非依赖于这个实现.

只要IRepository这个接口不发生变化, Controller就不会被影响到. 这也就可能会较少对整个项目的影响.

 

Interface 表明的是 "是什么样的", 而实现表明的是 "如何去实现".

Interface一旦完成后是不多改变的.

针对使用Repository+UnitOfWork模式的项目结构, 有时会有一点错误的理解, 可能会把项目的结构这样划分:

这样一来, 从命名空间角度讲. 其实就是这样的:

高级别的包/模块依赖于低级别的包/模块.

也就违反了DIP原则, 因此若是想按原则执行, 就须要引进一个新的模块:

把全部的抽象相关的类都放在Core里面.

这样就知足了DIP原则.

因此咱们把项目稍微重构如下, 把合约/接口以及项目的核心都放在MyRestful.Core项目里:

 

好的, 此次先写道这里, 项目已经作好了最基本的准备, 其他功能的扩展会随着后续文章进行.

下面应该快要切入REST的正题了.

相关文章
相关标签/搜索