您的位置:首页 > 编程语言 > ASP

从头编写 asp.net core 2.0 web api 基础框架 (3)

2017-10-12 07:16 931 查看
第一部分: http://www.cnblogs.com/cgzl/p/7637250.html
第二部分:http://www.cnblogs.com/cgzl/p/7640077.html

Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratch

之前我介绍完了asp.net core 2.0 web api最基本的CRUD操作,接下来继续研究:

IoC和Dependency Injection (控制反转和依赖注入)

先举个例子说明一下:

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;
}


View Code

注入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:

[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是什么时候用呢?

分两种方式:

一、使用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)}发送了邮件");
}
}


然后再试一下看看结果:



这回就没问题了。

二、是通过环境变量控制配置文件

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的配置是非常的灵活的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: