只有累積,沒有奇蹟

2019年4月8日 星期一

[.NETCore] ASP.NET Core - ENVIRONMENT 使用環境變數

前言
假日上課時前同事詢問在 .NET 中要如何設定執行環境,在 ASP.NET Core 中可以自行定義環境變數值  ASPNETCORE_ENVIRONMENT  ,如果沒有特別設定該值則預設會是 Production,這一篇就分享在 .NET Core 中如何使用環境變數以及設定方式,若有問題或是錯誤的地方歡迎各位高手給予指導。

ENVIRONMENT 
首先,如果有開發 .NET Core 開發者相信大家一定看過下面這段代碼,在 .NET Core 新增 Web Application 後開啟  startup.cs  內的 Configure 可以看到下列代碼
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    // to do something
}
分享一下自己對於這段 Code 的理解,Configure 方法注入  IHostingEnvironment  至啟動邏輯中,接著使用  env.IsDevelopment  方法判斷目前環境是否為開發環境,如果是的話就會進入到 UseDeveloperExceptionPage 也就是開發環境的錯誤訊息頁面,在追根究柢往下看 env.IsDevelopment 做了什麼事,按下 F12 去查看代碼背後是使用 IHostingEnvironment 的  IsEnvironment  方法,代碼如下
/// <summary>
/// Checks if the current hosting environment name is <see cref="F:Microsoft.AspNetCore.Hosting.EnvironmentName.Development" />.
/// </summary>
/// <param name="hostingEnvironment">An instance of <see cref="T:Microsoft.AspNetCore.Hosting.IHostingEnvironment" />.</param>
/// <returns>True if the environment name is <see cref="F:Microsoft.AspNetCore.Hosting.EnvironmentName.Development" />, otherwise false.</returns>
public static bool IsDevelopment(this IHostingEnvironment hostingEnvironment)
{
  if (hostingEnvironment == null)
    throw new ArgumentNullException(nameof (hostingEnvironment));
  return hostingEnvironment.IsEnvironment(EnvironmentName.Development);
}
IsEnvironment 方法是去取得目前的環境變數  hostingEnvironment.EnvironmentName  看是不是 development,其中 EnvironmentName 就是今天的重點,在 .NET Core 中可以透過設定  ASPNETCORE_ENVIRONMENT  定義目前應用程式運行的環境,預設內建 Development、Staging 以及 Production 三種值,環境變數是應用程式用來取得當前正在運行的環境,並依照環境變數值做適當的配置,舉例來說如果是 Production 環境的話就啟用 cache、關閉錯誤訊息畫面確保資訊不外洩、產生相對應的 Log 與監控...等行為,以上行為在 Development 與 Staging 都不啟用,都可以依據需求來自行定義執行邏輯。

環境設定 
如果需要在本機環境模擬不同的環境,在 .NET Core 中可以透過設定  properties\launchSettings.json  檔案定義目前應用程式運行的環境,在.NET 應用程式執行啟動時會先讀取 launchSettings.json ,檔案中的 environmentVariables 會覆寫系統環境的設定值。
其中在專案按下 F5 執行應用程式時,可以設定執行方式是 IIS Express 或是 Project 執行,其中就可以設定  ASPNETCORE_ENVIRONMENT  變數,其各自可以定義不同的環境變數值,舉例來說我們開啟 launchSettings.json 檔案,就可以看到 commandName 屬性對應到上圖的下拉選單值 IIS Express 與 ProjectName ,IIS Express 環境變數為 Staging,如果用 Project 啟動的話環境變數則是 Development
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5627",
      "sslPort": 44308
    }
  },
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Staging"
      }
    },
    "EFCoreApplication": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/values",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "https://localhost:5001;http://localhost:5000"
    }
  }
}

設定方式 
以下說明修改專案的 launchSetting.json 與設定系統變數 ASPNETCORE_ENVIRONMENT  的設定方式
Visual Studo IDE 設定 launchSettings.json
在 Visual Studio 則可以提供 GUI 介面可以進行環境變數值設定,設定方式為在專案按右鍵 > property > debug,看到中間區塊可以修改變數值得內容

Windows 設定系統變數
Step 1 : 開啟控制台 > 系統及安全性 > 系統
Step 2 : 進階系統設定 > 進階 > 環境變數
Step 3 : 新增系統變數 > 變數名稱 ASPNETCORE_ENVIRONMENT,變數值 Development
設定完成,大功告成
使用 powershell 設定系統變數
語法如下
$Env:ASPNETCORE_ENVIRONMENT = "Development"

Web.Config
透過 Web.Config 設定
<PropertyGroup>
  <EnvironmentName>Development</EnvironmentName>
</PropertyGroup>

macOS 與 Linux
.NET Core 跨平台是眾所皆知的,因此在非 Windows 下當然也可以設定環境變數值,但由於手邊沒有 macOS (家裡窮) 以及 Linux 環境,因此如果有設定上的需求可以參考 MSDN 說明,傳送門

感想
在設定環境變數來說,整體上操作與設定都不難,算是蠻容易上手的,另外在 Configure 和 ConfigureServices 也支援 Configure<EnvironmentName> 和 Configure<EnvironmentName>Services 也支援指定環境方法來做特定版本的設定方式,這方式是看了 MSDN 官方說明才知道有這方式,如果有需要要也可以參考看看,Happy Coding :)

參考
在 ASP.NET Core 中使用多個環境

2019年4月6日 星期六

[.NETCore] ASP.NET Core DI 生命週期 LifeTime

前言
最近在 Code Review 時主管提到關於 ASP.NET Core 的一些基本觀念,發現自己對於 .Core DI Service lifetime 部分有些模糊,在 .NET Core 中整個框架建立在 DI ( Dependency Injection) 之上,到處都可以看到 DI 的影子,因此決定寫篇文章釐清自己的觀念,若有問題或是有更推薦的方法歡迎提出一起討論或是給予指導。

.NET Core DI
.NET Core 有提供內建 DI Framework,命名空間為  Microsoft.Extensions.DependencyInjection ,使用 DI 容器服務第一步需要先做註冊,在 .NET Core DI 是在  startup.cs  中的 ConfigureServices 使用  IServiceCollection  的擴充方法指定服務容器生命週期 (LifeTime),預設提供的擴充方法與說明為
  • Transient : 每次請求時都會產生新的 Instance
  • Scoped : 每個 http Request 都會產生一份 Instance
  • Singleton : 整個 Application 只會有一份 Instance
看完生命週期的說明是不是覺得很抽象,如果第一次看就看懂的話實在是太厲害了,為了說明上面三種生命週期的差異性,以下舉個簡單的範例讓大家更容易理解

首先,先建立.NET Core Console 專案,專案建立完畢後新增一個 interface 為 IOperation,其屬性 OperationId 為 guid 型別為唯一識別碼,以及其他不同生命週期的 interface 分別實作基本 IOperation 介面
public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation{}

public interface IOperationScoped : IOperation{}

public interface IOperationSingleton : IOperation{}

public interface IOperationSingletonInstance : IOperation{}
接著新增 Operation 類別並實作不同生命週期的介面,並定義如果未提供 guid 時自己產生一份新的 guid 
public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}
接下來我們在 program.cs main 方法新增 ServiceCollection instance 並註冊 IOperationScoped、IOperationTransient、IOperationSingleton 到對應的 AddScoped、AddTransient、AddSingleton 生命週期中,接著透過 service.BuildServiceProvider 方法新增 scope
using Microsoft.Extensions.DependencyInjection;
using System;

namespace EFCoreConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var services = new ServiceCollection()
            .AddScoped<IOperationScoped, Operation>()
            .AddTransient<IOperationTransient, Operation>()
            .AddSingleton<IOperationSingleton, Operation>();

            var provider = services.BuildServiceProvider();
            using (var scope1 = provider.CreateScope())
            {
                var p = scope1.ServiceProvider;
                Console.WriteLine("<SCOPE 1>");
                Console.WriteLine("---Test 1---");
                Console.WriteLine($"transient1: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope1    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton1: {p.GetService<IOperationSingleton>().OperationId}");
                Console.WriteLine(" ");
                Console.WriteLine("---Test 2---");
                Console.WriteLine($"transient2: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope2    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton2: {p.GetService<IOperationSingleton>().OperationId}");
                Console.WriteLine(" ");
            }

            using (var scope2 = provider.CreateScope())
            {
                var p = scope2.ServiceProvider;
                Console.WriteLine("<SCOPE 2>");
                Console.WriteLine("---Test 3---");
                Console.WriteLine($"transient3: {p.GetService<IOperationTransient>().OperationId}");
                Console.WriteLine($"scope3    : { p.GetService<IOperationScoped>().OperationId }");
                Console.WriteLine($"singleton3: {p.GetService<IOperationSingleton>().OperationId}");
            }

            Console.ReadKey();
        }
    }
}

輸出如下
<SCOPE 1>
---Test 1---
transient1: ee48347a-a819-402b-8e4b-2d0e6a480186
scope1    : b7d9f907-6f95-4cbb-8ccb-3a2fb247287a
singleton1: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779

---Test 2---
transient2: 03a58729-7efe-4394-a9ae-0197e4a14e5c
scope2    : b7d9f907-6f95-4cbb-8ccb-3a2fb247287a
singleton2: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779

<SCOPE 2>
---Test 3---
transient3: 7c7334af-62e4-4085-b7a5-9876865412c1
scope3    : fe99001b-3566-4850-bc17-6757e7051eeb
singleton3: 0c92f569-2b89-4be9-a57c-5d0fc2dd8779
透過以上測試與觀察輸出結果,可以得到以下結論
  • Transient : 每次請求時都會產生新的 Instance > 每次都不一樣
  • Scoped : 每個 http Request 都會產生一份 Instance > 相同 Scoped 時會相同,不同 Scoped 時會不一樣
  • Singleton : 整個 Application 只會有一份 Instance > 整個應用程式一份
示意圖

使用情境
簡單來說,可以依據情境做不同的生命週期註冊的動作,舉例當 EF Core 中的 DBContext 當未設定生命週期時預設就會是 Scoped 使用,可以跨 Service 不用重複 new instance;如果今天是與 Redis 建立單一連線情境,避免建立過多連線造成 Timeout,這時候就是用到 Singletion 設定;如果是每次 request 都是獨一的就可以使用 transient,當你工作上有需要都可以思考看看哪種生命週期最適合。

參考
.NET Core 中的相依性插入
全面理解 ASP.NET Core 依赖注入

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com