Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratchhtml
以前我介绍完了asp.net core 2.0 web api最基本的CRUD操做,接下来继续研究:git
IoC和Dependency Injection (控制反转和依赖注入)
先举个例子说明一下:github
好比说咱们的ProductController,须要使用Mylogger做为记录日志的服务,MyLogger是一个在设计时指定的具体的类,这就是说ProductController对MyLogger有一个依赖。MyLogger一般是在Constructor里面new出来的。假如ProductController还依赖于不少其余的Services,当有问题发生的时候,须要替换或修改MyLogger,那么ProductController的代码就须要更改了,这也违反了设计模式的原则(对修改关闭)。这样作呢,也不利于进行单元测试,单元测试的时候没法提供一个Mock(Mock就是在测试中对于某种不易构建的对象,创建的一个虚拟的版本,以方便测试)版本的MyLogger,由于咱们使用的是具体的类。而ProductController同时也控制着MyLogger的生命周期,这是紧耦合。这个时候,Ioc(Inversion of control 控制反转)就有用了!web
Ioc把为ProductController选择某个依赖项(具备Log功能的Service)的具体实现类(MyLogger就是可能的具体实现类之一)的这项工做委托给了外部的一个组件。数据库
那么上面讲的Ioc的这项工做是怎么来实现的呢?那就是Depedency Injection这个设计模式。json
Dependency Injection能够说是Ioc的一个特定的种类。windows
DI模式是使用一个特定的对象(Container 容器)来为目标类(ProductController)进行初始化并提供其所须要的依赖项(MyLogger)。Container管理者这些依赖项的生命周期。设计模式
下面举一个典型的例子:api
public class ProductController : Controller { private ILogger<ProductController> _logger; // interface 不是具体的实现类 public ProductController(ILogger<ProductController> logger) { _logger = logger; } 。。。。。 }
ProductController里面须要有一个Field来保留这个依赖项,这里就是指_logger,而_logger不是具体的实现类,它是一个interface,ProductController须要的是一个实现了ILogger<T>接口的类。服务器
看一下Constructor的代码,这种叫作Constructor注入。Constructor须要一个实现了ILogger<T>接口的类的实例,不是一个具体的类,仍是一个interface。Container就会为ProductController注入它的依赖项。
这样作的最终结果就是,松耦合!(ProductController没必要再为那些工做负责了,也和具体的实现类没有直接联系了)。这时,再须要替换和修改这些依赖项的时候仅须要改很是少的代码或者彻底不用改代码了。并且单元测试也能够简单的进行了,由于这些依赖项(ILogger)均可以被实现了ILogger接口的Mock的版原本替代了。
在asp.net core里面呢,Ioc和依赖注入是框架内置的,这点和老版本的asp.net web api 2.2不同,那时候咱们得使用像autofac这样的第三方库来实现Ioc和依赖注入。
在asp.net core里面有一些services是内置的而且已经在Container注册了,好比说记录日志用的Logger。其余的services也能够在container注册,这通常是在StartUp类里面的ConfigureServices方法来实现的,框架级以及应用级的services均可以加进来。
下面咱们就把内置的Logger服务注册进去。
使用内置的Logger
由于Logger是asp.net core 的内置service,因此咱们就不须要在ConfigureService里面注册了。若是是asp.net core 1.0版本的话,咱们须要配置一个或者多个Logger,可是asp.net core 2.0的话就不须要作这个工做了,由于在CreateDefaultBuilder方法里默认给配置了输出到Console和Debug窗口的Logger。这是源码:

public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment()) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if (args != null) { config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); }) .UseIISIntegration() .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }); if (args != null) { builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build()); } return builder; }
注入Logger
咱们能够在ProductController里面注入ILoggerFactory而后再建立具体的Logger。可是还有更好的方式,Container能够直接提供一个ILogger<T>的实例,这时候呢Logger就会使用T的名字做为日志的类别:
namespace CoreBackend.Api.Controllers { [Route("api/[controller]")] public class ProductController : Controller { private ILogger<ProductController> _logger; public ProductController(ILogger<ProductController> logger) { _logger = logger; } ......
若是经过Constructor注入的方式不可用,那么咱们也能够直接从Container请求来获得它:HttpContext.RequestServices.GetService(typeof(ILogger<ProductController>)); 若是你在Constructor写这句话可能会空指针,由于这个时候HttpContext应该是null吧。
不过仍是建议使用Constructor注入的方式!!!
而后咱们记录一些日志把:
[Route("{id}", Name = "GetProduct")] public IActionResult GetProduct(int id) { var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (product == null) { _logger.LogInformation($"Id为{id}的产品没有被找到.."); return NotFound(); } return Ok(product); }
Log记录时通常都分几个等级,这点我假设你们都知道吧,就不介绍了。
而后试一下:经过Postman访问一个不存在的产品:‘/api/product/22’,而后看看Debug输出窗口:
嗯,出现了,前边是分类,也就是ILogger<T>里面T的名字,而后是级别 Information,而后就是咱们记录的Log内容。
再Log一个Exception:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
[Route(
"{id}"
, Name =
"GetProduct"
)]
public
IActionResult GetProduct(
int
id)
{
try
{
throw
new
Exception(
"来个异常!!!"
);
var
product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
if
(product ==
null
)
{
_logger.LogInformation($
"Id为{id}的产品没有被找到.."
);
return
NotFound();
}
return
Ok(product);
}
catch
(Exception ex)
{
_logger.LogCritical($
"查找Id为{id}的产品时出现了错误!!"
, ex);
return
StatusCode(500,
"处理请求的时候发生了错误!"
);
}
}
|
记录Exception就建议使用LogCritical了,这里须要注意的是Exception的发生就表示服务器发生了错误,咱们应该处理这个exception并返回500。使用StatusCode这个方法返回特定的StatusCode,而后能够加一个参数来解释这个错误(这里通常不建议返回exception的细节)。
运行试试:
OK。
Log到Debug窗口或者Console窗口仍是比较方便的,可是正式生产环境中这确定不够用。
正式环境应该Log到文件或者数据库。虽然asp.net core 的log内置了记录到Windows Event的方法,可是因为Windows Event是windows系统独有的,因此这个方法没法跨平台,也就不建议使用了。
官方文档上列出了这几个建议使用的第三发Log Provider:
把这几个Log provider注册到asp.net core的方式几乎是一摸同样的,因此介绍一个就行。咱们就用比较火的NLog吧。
NLog
首先经过nuget安装Nlog:
注意要勾上include prerelease,目前还不是正式版。
装完以后,咱们就须要为Nlog添加配置文件了。默认状况下Nlog会在根目录寻找一个叫作nlog.config的文件做为配置文件。那么咱们就手动改添加一个nlog.config:
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="logfile" xsi:type="File" fileName="logs/${shortdate}.log" /> </targets> <rules> <logger name="*" minlevel="Info" writeTo="logfile" /> </rules> </nlog>
而后设置该文件的属性以下:
对于Nlog的配置就不进行深刻介绍了。具体请看官方文档的.net core那部分。
而后须要把Nlog集成到asp.net core,也就是把Nlog注册到ILoggerFactory里面。因此打开Startup.cs,首先注入ILoggerFactory,而后对ILoggerFactory进行配置,为其注册NLog的Provider:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // loggerFactory.AddProvider(new NLogLoggerProvider());
loggerFactory.AddNLog(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(); } app.UseStatusCodePages(); app.UseMvc(); }
针对LoggerFactory.AddProvider()这种写法,Nlog一个简单的ExtensionMethod作了这个工做,就是AddNlog();
添加完NLog,其他的代码都不须要改,而后咱们试下:
在如图所示的位置出现了log文件。内容以下:
自定义Service
一个系统中可能须要不少个自定义的service,下面举一个简单的例子,
创建LocalMailService.cs:
namespace CoreBackend.Api.Services { public class LocalMailService { private string _mailTo = "developer@qq.com"; private string _mailFrom = "noreply@alibaba.com"; public void Send(string subject, string msg) { Debug.WriteLine($"从{_mailFrom}给{_mailTo}经过{nameof(LocalMailService)}发送了邮件"); } } }
使用这个Service,咱们伪装在删除Product的时候发送邮件。
首先,咱们要把这个LocalMailService注册给Container。打开Startup.cs进入ConfigureServices方法。这里面有三种方法能够注册service:AddTransient,AddScoped和AddSingleton,这些都表示service的生命周期。
transient的services是每次请求(不是指Http request)都会建立一个新的实例,它比较适合轻量级的无状态的(Stateless)的service。
scope的services是每次http请求会建立一个实例。
singleton的在第一次请求的时候就会建立一个实例,之后也只有这一个实例,或者在ConfigureServices这段代码运行的时候建立惟一一个实例。
咱们的LocalMailService比较适合Transient:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddTransient<LocalMailService>(); }
如今呢,就能够注入LocalMailService的实例了:
namespace CoreBackend.Api.Controllers { [Route("api/[controller]")] public class ProductController : Controller { private readonly ILogger<ProductController> _logger; private readonly LocalMailService _localMailService; public ProductController( ILogger<ProductController> logger, LocalMailService localMailService) { _logger = logger; _localMailService = localMailService; }
[HttpDelete("{id}")] public IActionResult Delete(int id) { var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } ProductService.Current.Products.Remove(model); _localMailService.Send("Product Deleted",$"Id为{id}的产品被删除了"); return NoContent(); }
而后试一下:
嗯,没问题。
可是如今的写法并不符合DI的意图。因此修改一下代码,首先添加一个interface,而后让LocalMailService去实现它:
namespace CoreBackend.Api.Services { public interface IMailService { void Send(string subject, string msg); } public class LocalMailService: IMailService { private string _mailTo = "developer@qq.com"; private string _mailFrom = "noreply@alibaba.com"; public void Send(string subject, string msg) { Debug.WriteLine($"从{_mailFrom}给{_mailTo}经过{nameof(LocalMailService)}发送了邮件"); } } }
有了IMailService这个interface,Container就能够为咱们提供实现了IMailService接口的不一样的类了。
因此再创建一个CloudMailService:
public class CloudMailService : IMailService { private readonly string _mailTo = "admin@qq.com"; private readonly string _mailFrom = "noreply@alibaba.com"; public void Send(string subject, string msg) { Debug.WriteLine($"从{_mailFrom}给{_mailTo}经过{nameof(LocalMailService)}发送了邮件"); } }
而后回到ConfigureServices方法里面:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddTransient<IMailService, LocalMailService>(); }
这句话的意思就是,当须要IMailService的一个实现的时候,Container就会提供一个LocalMailService的实例。
而后改一下ProductController:
namespace CoreBackend.Api.Controllers { [Route("api/[controller]")] public class ProductController : Controller { private readonly ILogger<ProductController> _logger; private readonly IMailService _mailService; public ProductController( ILogger<ProductController> logger, IMailService mailService) { _logger = logger; _mailService = mailService; }
而后运行一下,效果和上面是同样的。
然而咱们注册了LocalMailService,那么CloudMailService是何时用呢?
分两种方式:
1、使用compiler directive:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #if DEBUG services.AddTransient<IMailService, LocalMailService>(); #else services.AddTransient<IMailService, CloudMailService>(); #endif }
这样写就是告诉compiler,若是是Debug build的状况下,那么就使用LocalMailService(把这句话归入编译的范围),若是是在Release Build的模式下,就是用CloudMailService。
那咱们就切换到Release Build模式(或者在DEBUG前边加一个叹号试试):
运行试试,竟然没起做用。随后发现缘由是这样的:
在Release模式下Debug.WriteLine将不会被调用,由于这是Debug Build模式下专有的方法。。。
那咱们就改一下Cloud'MailService,使用logger吧:
public class CloudMailService : IMailService { private readonly string _mailTo = "admin@qq.com"; private readonly string _mailFrom = "noreply@alibaba.com"; private readonly ILogger<CloudMailService> _logger; public CloudMailService(ILogger<CloudMailService> logger) { _logger = logger; } public void Send(string subject, string msg) { _logger.LogInformation($"从{_mailFrom}给{_mailTo}经过{nameof(LocalMailService)}发送了邮件"); } }
而后再试一下看看结果:
这回就没问题了。
2、是经过环境变量控制配置文件
asp.net core 支持各式各样的配置方法,包括使用JSON,xml, ini文件,环境变量,命令行参数等等。建议使用的仍是JSON。
建立一个appSettings.json文件,而后把MailService相关的常量存到里面:
{ "mailSettings": { "mailToAddress": "admin__json@qq.com", "mailFromAddress": "noreply__json@qq.com" } }
asp.net core 2.0 默认已经作了相关的配置,咱们再看一下这部分的源码:
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); 。。。。。。return builder; }
红色部分的config的类型是IConfigurationBuilder,它用来配置的。首先是要找到appSettings.json文件,asp.net core 2.0已经作好了相关配置,它默认会从ContentRoot去找appSettings.json文件。
而后使用AddJsonFile这个方法来添加Json配置文件,第一个参数是文件名;第二个参数optional表示这个配置文件是不是可选的,把它设置成false表示咱们没必要非得用这个配置文件;第三个参数reloadOnChange为true,表示若是运行的时候配置文件变化了,那么就当即重载它。
使用appSettings.json里面的值就须要使用实现了IConfiguration这个接口的对象。建议的作法是:在Startup.cs里面注入IConfiguration(这个时候经过CreateDefaultBuilder方法,它已经创建好了),而后把它赋给一个静态的property:
public class Startup { public static IConfiguration Configuration { get; private set; } public Startup(IConfiguration configuration) { Configuration = configuration; }
而后咱们把LocalMailService里面改一下:
public class LocalMailService: IMailService { private readonly string _mailTo = Startup.Configuration["mailSettings:mailToAddress"]; private readonly string _mailFrom = Startup.Configuration["mailSettings:mailFromAddress"]; public void Send(string subject, string msg) { Debug.WriteLine($"从{_mailFrom}给{_mailTo}经过{nameof(LocalMailService)}发送了邮件"); } }
经过刚才写的Startup.Configuration来访问json配置文件中的变量,根据json文件中的层次结构,第一层对象咱们取的是mailSettings,而后试mailToAddress和mailFromAddress,他们之间用冒号分开,表示它们的层次结构。
经过这种方法取获得的值都是字符串。
而后运行一下试试,别忘了把Build模式改为Debug:
嗯,没问题。
针对不一样环境选择不一样json配置文件里的值(不是选择文件,而是值)
针对不一样的环境选择不一样的JSON配置文件,要求这个文件的名字的一部分包含有环境的名称。
添加一个Production环境下的配置文件:appSettings.Production.json, 其中Production是环境的名称,在项目--属性--Debug 里面环境变量的值:
创建好appSettings.Production.json后,能够发现它被做为appSettings.json的一个子文件显示出来,这样很好:
{ "mailSettings": { "mailToAddress": "admin__Production@qq.com" } }
再看一下这部分的源码:
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
AddJsonFile方法调用的顺序很是重要,它决定了多个配置文件的优先级。这里若是某个变量在appSettings和appSettings.Production.json都有,那么appSettings.Production.json的变量会被采用,由于appSettings.Production.json文件是后来才被调用的。
其中env的类型是IHostingEnvirongment,它里面的EnvironmentName就是环境变量的名称,若是环境变量填写的是Production,那就是appSettings.Production.json。
这么写的做用就是若是是在Production环境下,那么appSettings.json里面的部分变量值就会被appSettings.Production.json里面也存在的变量的值覆盖。
试试:首先环境变量是Development:
而后改为Production,试试:
结果如预期。
综上,经过Compiler Directive(设置Debug Build / Release Build),并结合着不一样的环境变量和配置文件,asp.net core的配置是很是的灵活的。
转自:http://www.cnblogs.com/cgzl/p/7652413.html