【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (3) 【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (3)

【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (3)

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

相關文章
相關標籤/搜索