2018年11月20日 星期二

Blazor 1 開發環境的安裝與設定

Blazor 1 開發環境的安裝與設定

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。

Blazor 提供可以使用 .NET C# 程式語言來進行開發出網頁 SAP 類型的應用程式,他與微軟前一代 Silverlight 工具不同處在於使用 Blazor 開發出來的專案,不需要透過任網頁插件 Plugins 安裝到瀏覽器上,就可以直接在網頁上執行;這都需要憑藉著 WASM Web Assembly 這個標準,也就是說,現今大部分的瀏覽器都有支援 WASM,因此,透過 Blazor 開發出來的網頁專案,可以直接使用 .NET 開發環境所建置、編譯出來 組件 Assembly 檔案,直接在網頁上執行,而不是將這些 .NET C# 程式語言轉譯成為 JavaScript 語言來執行。在這篇文章中,將會說明如何安裝與設定一個 Blazor 的開發環境,並且使用 Visual Studio 2017 建立起第一個 Blazor,並且實際執行在瀏覽器上,接著,將會說明由 Blazor 專案樣本所建立起來的第一個專案的架構與這個專案內的程式碼是如何設計的。本篇文章的範例原始碼,可以從 BlazorFirstTime 取得。

安裝與設定

在這篇文章所使用的 Visual Studio 2017 為 15.9.1 版本,因此,將會建立起 Blazor 0.7.0 的專案;首先,請執行 Visual Studio Installer 這個程式,並且點選 [修改] 按鈕。
請在 [工作負載] 勾選 [.NET Core 跨平台開發] 項目,這樣會進行安裝 NET Core 2.1 SDK
請在 [工作負載] 勾選 [ASP.NET 與網頁程式開發] 項目
最後,點選右下角的 [修改] 按鈕,開啟進行 Blazor 開發環境所需要的基本套件。
一旦相關 Blazor 開發環境所需要的基本套件安裝完成之後,還需要安裝一個 Blazor 的 Visual Studio 市集 Marketplace 擴充功能套件,請使用瀏覽器打開 ASP.NET Core Blazor Language Services 網頁,下載安裝這個 Viusal Studio 擴充功能。
若想要使用 Visual Studio Code 來進行開發 Blazor 專案,需要安裝 dotnet 命令列下使用的專案樣板,因此,打開 [命令提示字元視窗] ,輸入底下命令
dotnet new -i Microsoft.AspNetCore.Blazor.Templates

建立測試專案

若已經將 Blazor 開發工具安裝與設定完成之後,現在可以開始建立第一個 Blazor 開發專案。
  • 啟動 Visual Studio 2017
  • 點選功能表的 [檔案] > [新增] > [專案] 功能表選項
  • 在 [新增專案] 對話窗中左邊區域,選擇 [已安裝] > [Visual C#] > [Web] > [ASP.NET Core Web 應用程式]
  • 在對話窗的下方的 [名稱] 欄位中,輸入這個練習專案的名稱 BlazorFirstTime
  • 在對話窗的下方的 [位置] 欄位中,選擇這個專案要儲存的檔案路徑
  • 最後點選對話窗的右下方 [確定] 按鈕
  • 在 [新增 ASP.NET Core Web 應用程式] 對話窗左上方區域,在第一個下拉選單,選擇 [.NET Core] 在第二個下拉選單,請選擇最新的 ASP.NET Core 版本,在現在這個時間點,可以選擇 [ASP.NET Core 2.1]
  • 在對話窗中間區域,請點選 [Blazor] 這個項目
  • 最後點選對話窗的右下方 [確定] 按鈕

執行 Blazor 專案

請按下 [Ctrl + F5] 按鈕,開始執行這個 Blazor 專案,此時,瀏覽器將會開啟,顯示這個 Blazor 專案所設計的頁面,如下圖所示。在這個專案樣板所建立的 Blazor 專案,將會展示三個透過 Blazor 元件 Component 所設計的功能,分別是:Home, Counter, Fetch Data。

Blazor 專案架構

現在,請在 Visual Studio 的方案總管視窗上,來檢視 Blazor 專案樣板所建立的專案,是提供了那些檔案內容,以便可以做到上面的網頁效果;從下面方案總管螢幕截圖上,可以看到有兩個資料夾,Pages 資料夾,這裡是要存放 Blazor 元件 Component 的地方,這些元件檔案都是 Razor 類型的檔案,當 Blazor 專案建置成功之後,每個元件檔案 (.cshtml) 將會產生出 C# 類別;第二個資料夾則是 Shared 資料夾,這裡將會存放著共用檔案之用,例如,整個頁面的版面配置 Layout 用的 MainLayout.cshtml 與最左方的功能表選項 NavMenu.cshtml 。
對於該網頁會用到的靜態檔案,如 CSS, JavaScript, 圖片等等,將都會存放在 wwwroot 這個節點之內。而在專案根目錄與 Page 目錄下,都會看到有 _ViewImports.cshtml 這個檔案,其為 Razor 共用指示詞,這對於許多檢視都會用到共用的指示詞,可以定義在這個 _ViewImports.cshtml 檔案中,例如:@addTagHelper@removeTagHelper@tagHelperPrefix@using@model@inherits@inject;最後兩個檔案,那就是 Programs.cs 與 Startup.cs 檔案,這兩個檔案與 ASP.NET Core 專案中的同樣的檔案,將會提供同樣的服務與功能。
對於這個專案的設定檔案,也是相當的精簡,使用滑鼠右鍵點選該專案的節點,選擇 [編輯 BlazorFirstTime.csproj],會出現下面的 XML 內容;在這個 .csproj 檔案內,加入了兩個參考套件: Microsoft.AspNetCore.Blazor.Browser 與 Microsoft.AspNetCore.Blazor.Build ,這兩個套件則是要在開發 Blazor 專案時候會用的。
 BlazorFirstTime.csproj 檔案
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <RunCommand>dotnet</RunCommand>
    <RunArguments>blazor serve</RunArguments>
    <LangVersion>7.3</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Blazor.Browser" Version="0.7.0" />
    <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" PrivateAssets="all" />

    <DotNetCliToolReference Include="Microsoft.AspNetCore.Blazor.Cli" Version="0.7.0" />
  </ItemGroup>

</Project>

Blazor 應用程式啟動流程

現在來看看 Blazor 專案的程式進入點 Program Entry Point ,也就是 Program.cs 檔案,如同 .NET Core 的專案相同,Blazor 專案的程式進入點也是 Main 這個靜態方法,而且在這裡比起 ASP.NET Core 類型專案,顯得更加的精簡,只要在使用 UseBlazorStartup 這個泛型方法,就可以開始啟動與執行 Blazor 專案了。
 Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
        BlazorWebAssemblyHost.CreateDefaultBuilder()
            .UseBlazorStartup<Startup>();
}
同樣的,Blazor 一樣使用 OWIN 框架來設定這個專案的運作與起的功能,不過,可以看到,在底下的 Startup.cs 這個檔案中,程式碼也是相當的精簡,若要設定 .NET Core IoC 容器 Container 的型別註冊,也就是自己開發的抽象介面與具體實作類別對應,可以在 ConfigureServices 這個方法內來執行相關的陳述式。
 Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }
    public void Configure(IBlazorApplicationBuilder app)
    {
        app.AddComponent<App>("app");
    }
}
對於根目錄下的 App.cshtml 檔案,在這裡所進行宣告的這些東西是暫時的,之後將會把這裡的宣告移動到 Program.cs ,因此,可以忽略掉這個檔案內容。
當 Blazor 專案啟動之後,可以透過瀏覽器來執行 Blazor 專案的應用程式,預設的開啟網頁將會位於 [wwwroot] 資料夾內的 index.html 檔案,底下為這個檔案的內容。在這裡除了對於網頁畫面的設計使用到 bootstrap 功能,最重要的是這裡會執行 blazor.webassembly.js 這個 JavaScript,這個檔案將會於建置 Blazor 專案的時候會產生出來,所以,一旦 Blazor 專案建置完成之後,可以在方案總管視窗中,點選工具列上的 [顯示所有檔案] 按鈕,就可以在 [bin] > [Debug] > [netstandard2.0] > [dist] > [_framework] 目錄下看到這個檔案。
這個 blazor.webassembly.js JavaScript 將會下載 blazor.boot.json 這個檔案,這裡將會描述 Blazor 專案的 .NET 組件的程式進入點,以及會參考到那些其他 .NET 組件: {"main":"BlazorFirstTime.dll","entryPoint":"BlazorFirstTime.Program::Main","assemblyReferences":["Microsoft.AspNetCore.Blazor.Browser.dll","Microsoft.AspNetCore.Blazor.dll","Microsoft.AspNetCore.Blazor.TagHelperWorkaround.dll","Microsoft.Extensions.DependencyInjection.Abstractions.dll","Microsoft.Extensions.DependencyInjection.dll","Microsoft.JSInterop.dll","Mono.WebAssembly.Interop.dll","mscorlib.dll","System.Core.dll","System.dll","System.Net.Http.dll","BlazorFirstTime.pdb"],"cssReferences":[],"jsReferences":[],"linkerEnabled":true} ,透過這個檔案的描述,便會開始逐一將這些 .NET 組件 Assembly 檔案下載到瀏覽器本地端。
想要看到開啟一個 Blazor 網頁的時候,會下載來些內容與檔案,可以在瀏覽器下按下 [F12] 按鈕,切換到 [網路] 標籤頁次,將會看如下面截圖的更加詳盡的資訊。blazor.webassembly.js 也會下載 mono.wasm 檔案,這是一個 Mono 框架,使用 Wasm 設計出來的 .NET CLR 執行環境,有了這個檔案,就可以開始執行 blazor.boot.json 所提到的各個 .NET 組件 Assembly 檔案了。
因此,Blazor 並不是為了要能夠讓 C# 程式語言可以在網頁上執行,而把 C# 程式語言轉換成為 JavaScript 程式語言,而是把 Blazor 專案內的所有 .NET 組件檔案,透過使用 Wasm 開發出來的 .NET CLR 環境,直接在瀏覽器上執行 .NET 的程式碼;這與微軟之前的 Silverlight 開發框架有所不同,因為想要在瀏覽器上執行 Silverlight 的程式,必須要能夠在瀏覽器上安裝一個 Silverlight 插件,才能夠執行 Silverlight 的程式,而 Blazor 開發出來的程式,則不需要安裝任何插件到瀏覽器上,就可以直接執行 .NET C# 程式碼。
一旦 Blazor 應用程式啟動完成之後,將會透過 DOM 搜尋網頁上的 App 標籤 Tag,將第一個要顯示的 Blazor 元件所產生的相關網頁內容,替換到 App 標籤的 Loading... 文字,所以,當 Blazor 顯示完成之後,將會看到如下圖的 HTML 元素出現在網頁上。
 index.html 檔案內容
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width">
    <title>BlazorFirstTime</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>

    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
Index.cshtml 是個 Razor 檔案,透過 Razor 語法來嵌入到 HTML 標記宣告語言內,就可以在這裡撰寫 C# 程式語言了。
 Index.cshtml
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />
在這個由專案樣板產生的範例專案中,提供了三個由 Blazor 元件所設計出來的畫面,第一個元件 Index.cshtml 則是說明如何使用元件
第二個元件則是 Counter.cshtml 將會展示如何撰寫出一個按鈕元素,並且使用 C# 程式語言來設計該按鈕的點選事件,在這以往網頁應用程式開發環境下,一定需要透過 JavaScript 程式語言的呼叫,才能夠完成這樣的需求,如今,透過 Blazor 的技術,可以使用 C# 程式語言就可以直接做到了。
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}
最後一個 Blazor 元件為 FetchData.cshtml ,在這裡將會說明如何使用 Blazor 的生命週期事件,當這個元件已經準備好了,將會觸發 OnInitAsync 非同步事件,因此,可以在這個事件內使用相依性注入設計模式,取得 HttpClient 這個類別實作物件,進行網路資源存取,並且配合 Razor 語法將這抓取到遠端 JSON 資料,把這些集合物件顯示在瀏覽器上。
@page "/fetchdata"
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@functions {
    WeatherForecast[] forecasts;

    protected override async Task OnInitAsync()
    {
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>("sample-data/weather.json");
    }

    class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public int TemperatureF { get; set; }
        public string Summary { get; set; }
    }
}


2018年11月17日 星期六

ASP.NET Core IoC 容器與整合 Unity 容器之相依性注入之應用

ASP.NET Core IoC 容器與整合 Unity 容器之相依性注入之應用

ASP.NET Core 的專案將會大量使用到相依性注入 Dependency Injection 這個設計模式 Design Pattern,也就是說,若身為開發者的你對於物件導向的控制反轉設計 Inversion of Control (IoC)、SOLID原則的相依反轉原則 Dependency Inversion Principle (DIP) 、相依性注入 Dependency Injection、IoC 容器 Container、各種注入實作物件的方式並不瞭解,那麼,則是很不幸的,對於進行 ASP.NET Core 專案開發的時候,將會無法充分發揮各種好用的功能,因此,在本篇文章中,將會對於如何使用 ASP.NET Core 提供的內建 IoC 容器的用法進行使用方式說明,並且,透過 Unity 這個擴充的 DI Container / IoC Container 相依性注入容器是要如何安裝、設定與在 ASP.NET Core 專案上來使用,都將會透過實際開發專案程式碼進行說明。本篇文章的範例原始碼,可以從 ASPNETCoreIoCDI 取得。

建立測試專案

首先,先來將這個範例專案建立出來,並且看到這個專案的執行結果,稍後將會對於這些操作過程進行解說。
  • 啟動 Visual Studio 2017
  • 點選功能表的 [檔案] > [新增] > [專案] 功能表選項
  • 在 [新增專案] 對話窗中左邊區域,選擇 [已安裝] > [Visual C#] > [Web] > [ASP.NET Core Web 應用程式]
  • 在對話窗的下方的 [名稱] 欄位中,輸入這個練習專案的名稱 ASPNETCoreIoCDI
  • 在對話窗的下方的 [位置] 欄位中,選擇這個專案要儲存的檔案路徑
  • 最後點選對話窗的右下方 [確定] 按鈕
  • 在 [新增 ASP.NET Core Web 應用程式] 對話窗左上方區域,在第一個下拉選單,選擇 [.NET Core] 在第二個下拉選單,請選擇最新的 ASP.NET Core 版本,在現在這個時間點,可以選擇 [ASP.NET Core 2.1]
  • 在對話窗中間區域,請點選 [API] 這個項目
  • 右下方的 [驗證] 欄位,請確認為 [無驗證]
  • 最後點選對話窗的右下方 [確定] 按鈕
  • 在專案建立完成之後,滑鼠右擊專案節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: ASPNETCoreIoCDI] 視窗,點選 [瀏覽] 標籤頁次,在搜尋文字輸入盒,輸入[Unity.Microsoft.DependencyInjection] ,當搜尋到這個 NuGet 套件之後,點選該項目,接著點選 [安裝] 按鈕,將這個套件安裝到這個專案內。
  • 滑鼠右擊專案節點,選擇 [加入] > [類別] 選項
  • 在 [新增項目] 對話窗中間區域內,點選 [介面],在對話窗下方名稱欄位,輸入 [IMyDependency] ,最後點選 [新增] 按鈕
C Sharp / C#
public interface IMyDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
public class MyClass1 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
public class MyClass2 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass2";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
  • 在 [新增項目] 對話窗中間區域內,點選 [介面],在對話窗下方名稱欄位,輸入 [IDefaultDependency] ,最後點選 [新增] 按鈕
C Sharp / C#
public interface IDefaultDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
public class MyDefaultClass1 : IDefaultDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyDefaultClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
  • 打開 [Startup.cs] 檔案,使用底下程式碼來建立一個 Container 屬性
C Sharp / C#
public static IUnityContainer Container { get; set; }
  • 打開 [Startup.cs] 檔案內,使用底下程式碼來新增 ConfigureContainer 方法
C Sharp / C#
public IServiceProvider ConfigureServicesIServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddScoped<IDefaultDependency, MyDefaultClass1>();
    services.AddTransient<IMyDependency, MyClass2>();
    Unity.Microsoft.DependencyInjection.ServiceProvider serviceProvider =
        Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services)
        as Unity.Microsoft.DependencyInjection.ServiceProvider;
    ConfigureContainer(Container=(UnityContainer)serviceProvider);
    return serviceProvider;
}
  • 打開 [Startup.cs] 檔案,找到 ConfigureServices 方法,將這個方法修改成為底下程式碼
C Sharp / C#
public void ConfigureContainer(IUnityContainer ontainer)
{
    // Could be used to register more types
    container.RegisterType<IMyDependency, MyClass1>("MyClass1");
}
  • 在 [Controllers] 資料夾內,找到並且打開 [ValuesController.cs] 檔案,將 ValuesController 這個類別,使用底下程式碼來替換。
C Sharp / C#
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IMyDependency myDependency1;
    private readonly IMyDependency myDependency2;
    private readonly IServiceProvider serviceProvider;
    private readonly IUnityContainer container;
    private readonly IDefaultDependency defaultDependency1;
    private readonly IDefaultDependency defaultDependency2;
    public ValuesController(IMyDependency myDependency1, IMyDependency myDependency2,
        IServiceProvider serviceProvider, IUnityContainer container,
        IDefaultDependency defaultDependency1, IDefaultDependency defaultDependency2)
    {
        this.myDependency1 = myDependency1;
        this.myDependency1.InstanceFrom = "控制器的建構式注入";
        this.myDependency1.InstanceName = "myDependency1";
        this.myDependency2 = myDependency2;
        this.myDependency2.InstanceFrom = "控制器的建構式注入";
        this.myDependency2.InstanceName = "myDependency2";
        this.serviceProvider = serviceProvider;
        this.container = container;
        this.defaultDependency1 = defaultDependency1;
        this.defaultDependency1.InstanceFrom = "控制器的建構式注入";
        this.myDependency1.InstanceName = "myDependency1";
        this.defaultDependency2 = defaultDependency2;
        this.defaultDependency2.InstanceFrom = "控制器的建構式注入";
        this.defaultDependency2.InstanceName = "defaultDependency2";
    }
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<object>> Get()
    {
        IServiceProvider contextServiceProvider = HttpContext.RequestServices;
        IMyDependency myDependency3 = contextServiceProvider.GetService<IMyDependency>();
        myDependency3.InstanceFrom = "由 HttpContext 取得 IServiceProvider 進而手動解析";
        myDependency3.InstanceName = "myDependency3";
        IMyDependency myDependency4 = serviceProvider.GetService<IMyDependency>();
        myDependency4.InstanceFrom = "由 建構式注入 取得 IServiceProvider 進而手動解析";
        myDependency4.InstanceName = "myDependency4";
        IMyDependency myDependency5 = container.Resolve<IMyDependency>();
        myDependency5.InstanceFrom = "由 建構式注入 取得 IUnityContainer 進而手動解析(預設匿名解析)";
        myDependency5.InstanceName = "myDependency5";
        IMyDependency myDependency6 = container.Resolve<IMyDependency>("MyClass1");
        myDependency6.InstanceFrom = "由 建構式注入 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)";
        myDependency6.InstanceName = "myDependency6";
        IMyDependency myDependency7 = container.Resolve<IMyDependency>();
        myDependency7.InstanceFrom = "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(預設匿名解析)";
        myDependency7.InstanceName = "myDependency7";
        IMyDependency myDependency8 = Startup.Container.Resolve<IMyDependency>("MyClass1");
        myDependency8.InstanceFrom = "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)";
        myDependency8.InstanceName = "myDependency8";
        return new object[] { myDependency1, myDependency2, myDependency3, myDependency4,
        myDependency5, myDependency6, myDependency7, myDependency8,defaultDependency1,defaultDependency2};
    }
    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }
    // POST api/values
    [HttpPost]
    public void Post([FromBody] string value)
    {
    }
    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody] string value)
    {
    }
    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
}
  • 執行這個專案,將會看到網頁會有底下輸出結果
Console
[
  {
    "myProperty": "014ef344-2619-44b7-8a7b-c94845e4c978",
    "className": "MyClass2",
    "instanceFrom": "控制器的建構式注入",
    "instanceName": "myDependency1"
  },
  {
    myProperty: "408b4b5e-cca7-48cc-b7dc-c15444d143e7",
    className: "MyClass2",
    instanceFrom: "控制器的建構式注入",
    instanceName: "myDependency2"
  },
  {
    myProperty: "cfaf1568-591d-4a66-aca1-f51f461bbf05",
    className: "MyClass2",
    instanceFrom: "由 HttpContext 取得 IServiceProvider 進而手動解析",
    instanceName: "myDependency3"
  },
  {
    myProperty: "89453a9e-e322-48ae-8442-0804792e0bf7",
    className: "MyClass2",
    instanceFrom: "由 建構式注入 取得 IServiceProvider 進而手動解析",
    instanceName: "myDependency4"
  },
  {
    myProperty: "757edf1d-50e9-4ce5-b1f1-1836981b3bf0",
    className: "MyClass2",
    instanceFrom: "由 建構式注入 取得 IUnityContainer 進而手動解析(預設匿名解析)",
    instanceName: "myDependency5"
  },
  {
    myProperty: "bdc09609-8a2b-48ac-909b-ff85a74a50fd",
    className: "MyClass1",
    instanceFrom: "由 建構式注入 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)",
    instanceName: "myDependency6"
  },
  {
    myProperty: "15b124d1-3d88-432f-bb1c-fe6a1ccb253e",
    className: "MyClass2",
    instanceFrom: "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(預設匿名解析)",
    instanceName: "myDependency7"
  },
  {
    myProperty: "8567b2c6-b71f-4259-a536-846ff47f3809",
    className: "MyClass1",
    instanceFrom: "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)",
    instanceName: "myDependency8"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
]

物件導向的 Inversion of Control (IoC) 控制反轉

在要了解如何在 ASP.NET Core 專案內使用相依性注入的用法,對於專案內要使用的許多類別,為了降底這些類別的耦合力,因此,需要採用物件導向 Object-oriented programming (OOP) 的控制反轉的技術來進行這些類別的設計;也就是說,為了要把這些相依的類別間做到低耦合力關係,需要針對這些類別需求,進行資訊抽象化的設計,也就是在整個專案設計的程式碼內,都會針對抽象型別來設計專案程式碼,這使得在進行專案開發的時候,因為都是針對這些抽象型別來進行程式碼開發,所以,也就會與真正的具體實作類別沒有密切的關係,也就是說,到時候是由哪個具體實作類別來進行產生出執行個體 Instance 物件,並不會在設計類別的時候來進行考量與產生出這些執行個體,當然,在這個類別內應該也不會看到了 new SomeClass() 這樣用產生一個執行個體的表示式出現了。
不過,身為開發者也都知道,在 C# 程式語言中的抽象型別可以宣告為 abstract 的抽象類別或者是宣告為 interface 的介面,當一個型別宣告為 abstract 或者 interface 的時候,是不能夠透過 new 運算子來產生出執行出該型別的執行個體的,那麼,要如得到這些抽象型別的執行個體呢?這就需要透過相依性注入的設計模式。
在這個 ASP.NET Core 專案內的 ValuesController 控制器類別內,將會用到 MyClass1 與 MyDefaultClass1 這兩個類別所產生的執行個體,所以,第一個步驟就是要將這兩個類別進行資訊抽象化,對於 MyClass1 這個類別,將會設計出底下 介面 Interface 。在這個介面宣告了三個 屬性 Property,對於 MyProperty 這個屬性是屬於 Guid 型別,將會用來檢視這個由具體實作類別所產生的物件是否只會產生一次或者在每次都會產生一個新的執行個體;對於 InstanceFrom 這個屬性是屬於字串 string 型別,會用來說明產生的執行個體是由哪個具體實作類別所產生的;對於最後一個 InstanceName 這個屬性也是屬於字串型別,將會標示出這個執行個體的 C# 變數名稱。
C Sharp / C#
public interface IMyDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
當設計出介面型別之後,接下來就需要使用這個介面來實作出類別,在這個專案內,使用這個語法 public class MyClass1 : IMyDependency 定義一個 MyClass1 這個類別,並且實作 IMyDependency 這介面;從底下的 MyClass1 定義程式碼,對於 MyProperty 這個類別,將會使用 含有支援欄位的屬性 方法來進行定義,在這個支援欄位會使用 Guid.NewGuid() 產生預設值,也就是說,當系統要使用 MyClass1 類別產生出多個執行個體的時候,對於 MyProperty 屬性值將會有不同的內容,而若發現多個屬於 MyClass1 的 C# 變數,他們的 MyProperty 的值都是相同的,則表示這些變數都是指向同一個 MyClass1 的執行個體。
C Sharp / C#
public class MyClass1 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
另外,在這裡也要說明當在 ASP.NET Core 專案中,需要用到多個具體實作類別都實作同一個介面情境,而當要注入這個介面的時候,究竟會產生出哪個具體實作的類別來產生出最終執行個體,因此,將會定義出 MyClass2 這個類別,也是實作出同一個 IMyDependency 這個介面,不過,對於 MyClass2 這個類別的 ClassName 屬性,將會預設指定為 MyClass2 這個字串名稱,這樣,我們就可以透過這個屬性值來了解到這個物件是由哪個具體實作類別所生成的,當然,也可透過當時的執行個體來呼叫 GetType() 方法,取得具體實作的類別名稱。
C Sharp / C#
public class MyClass2 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass2";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
最後,要能夠了解相依性注入這個設計模式中相當重要的一個設計技術,那就是生命週期管理,在這個專案將會設計一個 MyDefaultClass1 類別,將會實作 IDefaultDependency 介面,而對於 MyDefaultClass1 類別與IDefaultDependency 介面的設計內容將都會與前面討論到的 MyClass1 類別與 IMyDependency 介面相同。

預設 .NET Core 的 IoC 容器用法

當把專案內用到的各個類別,進行資訊抽象化作業,設計出相關的抽象型別,這裡使用的是 C# 介面 Interface 型別,並且這些類別都有實作出這些介面,接下來的工作將會需要使用 ASP.NET Core 專案中預設提供的 IoC 容器 Container (也可以稱做 相依性注入容器 DI Container,這兩個名詞代表了同一件事情),進行相依性注入中最為重要的 註冊 Registration 動作。通常來說,需要在專案的 組合根 (Composition Root) 地方來進行這個型別對應註冊的工作,通常來說,組合根 (Composition Root) 一般都是很接近於程式進入點 Entry Point 的附近。
在 ASP.NET Core 專案內,可以透過這個 Startup.cs 檔案內的 Startup 類別來進行抽象型別與具體實作型別的註冊工作,另外,在進行相依性注入要用到的型別註冊工作時候,也需要一併指定當要生成這些抽象型別之具體實作類別的執行個體的時候,是要每次要求注入同一個介面物件的時候,都要自動生成一個具體執行個體、還是在整個應用程式都在執行的工作成,每次注入同一個介面的時候,都只會得到同一個執行個體、又或者是當在同一個 HTTP 要求 Request 動作產生的時候,對於相同介面要注入多個物件的時候,都會只產生一個執行個體物件,不過,對於另外一次 HTTP 要求產生的時候,將會對於相同介面要注入多個物件的時候,又會只產生一個執行個體物件

註冊抽象型別與具體實作型別

想要進行型別的註冊動作,請打開 Startup.cs 檔案,找到 ConfigureServices(IServiceCollection services) 這個方法,在這個 ConfigureServices 方法內,將會有個 IServiceCollection services 參數,此時,可以透過這個參數 servers 使用底下的三個方法來進行抽象型別與具體實作型別的註冊。
  • IServiceCollection.AddTransient
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為暫時性 Transient,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,都會實際產生與建立出這個執行個體。
  • IServiceCollection.AddScoped
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為具範圍 Scoped,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,只要這些注入行為發生在同一個 HTTP 要求過程中,對應的具體實作類別所產生的執行個體只會產生一次而且只對在第一次要求注入該抽象型別的時候來建立該執行個體,也就是說,同一個 HTTP 要求中,若有多次注入同一個抽象型別的動作,得到的執行個體都是同一個;但是,當另外一次 HTTP 要求發生的時候,又會產生一個新的執行個體。
  • IServiceCollection.AddSingleton
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為單一 Singleton,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,不論是在哪個 HTTP 要求過程中,只有在該應用程式執行後,第一次要求注入的時候,會建立該具體實作類別的執行個體,之後要注入該抽象型別的時候,就都會取回剛剛建立的同一個執行個體。
在這個範例專案中,可以從 ConfigureServices(IServiceCollection services) 方法內,看到底下兩行陳述式,第一行陳述式將會進行介面 IDefaultDependency 與 類別 MyDefaultClass1 的對應註冊,也就是說,當 IoC 容器被要求注入一個 IDefaultDependency 介面物件的時候,將會回傳一個 MyDefaultClass1 具體實作類別的執行個體,此處因為使用了 AddScoped 方法來進行註冊。
C Sharp / C#
services.AddScoped<IDefaultDependency, MyDefaultClass1>();
services.AddTransient<IMyDependency, MyClass2>();
因此,只要在同一個 HTTP 要求過程中,都只會回傳同一個 MyDefaultClass1 的執行個體,因此,當執行這個範例專案的時候,將會發現到對於屬性名稱為 className 的屬性值為 MyDefaultClass1 名稱的兩個物件,也就是在 ValuesController 類別中透過建構式注入的兩個欄位 defaultDependency1, defaultDependency2,這兩個欄位都透過建構式注入得到同一個物件,這可以從這兩個欄位的 myProperty 屬性值都是同一個 Guid 值來判斷出來。
Console
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
請將 services.AddScoped<IDefaultDependency, MyDefaultClass1>(); 陳述式修改為 services.AddTransient<IDefaultDependency, MyDefaultClass1>();,並且再度執行一次這個專案,查看屬性名稱為 className 的屬性值為 MyDefaultClass1 名稱的兩個物件輸出結果將會如下,他們的 myProperty 的 Guid 屬性值都是不相同,因此,可以判對出當使用 AddTrasient 這個方法來進行抽象型別與具體型別註冊後,透過 IoC 容器來注入這個抽象介面的具體實作物件的時候,每個要求注入需求動作都會實際建立起一個新的執行個體,這與上面使用 AddScoped 進行註冊的結果會有所不同的。
Console
  {
    myProperty: "8eead000-1f76-4d0a-b543-0ad6627e0394",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency1"
  },
  {
    myProperty: "5c034da7-c20b-48e2-bbfd-ff50d84f662c",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
還有,對於介面 IMyDependency 與具體實作類別 MyClass2 ,將會使用這個陳述式來進行註冊,services.AddTransient<IMyDependency, MyClass2>();,也就是當要透過 IoC 容器進行注入 IMyDependency 介面的時候,每次都會建立一個新的具體實作類別執行個體出來。
最後,當要進行註冊到 IoC 容器內的時候,也可以只指定具體類別,例如:services.AddTransient<MyClass2>();,這表示當透過 IoC 容器注入一個 MyClass2 類別的時候,IoC 容器可以自動建立一個這樣的執行個體出來。

使用建構式注入來注入實作物件

當需要透過 IoC 容器進行注入具體實作物件的時候,一般來說,可以透過 建構式注入、屬性注入、方法注入這三種方式來取得取體實作物件,不過,在 ASP.NET Core 內,僅支援 建構式注入 這種方式來注入取得具體實作的執行個體;但是,若想要使用另外兩種注入方式,可以透過其他的 IoC 容器來完成,在這裡將會使用 Unity 這個 DI 容器來做到這樣的需求。
當要使用的類別,其是透過 ASP.NET Core 開發框架來產生出一個執行個體,例如:繼承 ControllerBase 的相關控制器類別,而不是自己使用 new 運算子自己建立出來的執行個體,可以在該類別內的建構函式,將要注入介面放到該建構函式內參數內,如同底下的程式碼。在這裡,將會要透過 IoC 容器自動注入 IMyDependency, IMyDependency, IServiceProvider, IUnityContainer, IDefaultDependency, IDefaultDependency 這些物件作為該建構函式的參數。
因為所有要透過 IoC 容器來自動注入的物件,都需要事先在 組合根 (Composition Root) 地方進行註冊,因為,在這裡 IoC 容器將會知道 IMyDependency, IMyDependency, IDefaultDependency 的具體時最類別是哪個,不過,在前面的 ConfigureServices(IServiceCollection services) 方法內,似乎沒有進行 IServiceProvider, IUnityContainer 這兩個抽象介面的註冊,那麼,在這裡要注入甚麼具體實作的執行個體呢?
若在建構函式內所引用的參數,其抽象介面並沒有在 IoC 容器中進行註冊,則當透過 IoC 容器要注入這個介面時候,就會拋出例外異常錯誤;不過對於 IServiceProvider, IUnityContainer 這兩個抽象介面,前者是 ASP.NET Core 開發框架自動註冊進去的,後者則是安裝了 Unity 這個 DI 容器套件 (Unity.Microsoft.DependencyInjection) 自動註冊進去的,因此,可以放心在建構是的參數內直接使用。
在這個建構函式參數中,將會使用到兩次 IDefaultDependency 這個介面,在這裡是要用來檢測相依性注入的物件生命週期管理使用;若當註冊 IDefaultDependency 這個抽象介面的時候,使用的是 AddScoped 方法來進行註冊,則對於 defaultDependency1 與 defaultDependency2 這兩個參數,將會指向同一個執行個體,而當使用 AddTransient 方法來進行註冊,則對於 defaultDependency1 與 defaultDependency2 這兩個參數,將會指兩個完全不同的執行個體。
這個 ValuesController 類別的建構式內,將會進行注入物件的一些初始化設定,這些設定程式碼將是用來標示這個變數物件名稱是甚麼以及是透過甚麼方式來注入到這個類別內的。
C Sharp / C#
public ValuesController(IMyDependency myDependency1, IMyDependency myDependency2,
    IServiceProvider serviceProvider, IUnityContainer container,
    IDefaultDependency defaultDependency1, IDefaultDependency defaultDependency2)
{
    this.myDependency1 = myDependency1;
    this.myDependency1.InstanceFrom = "控制器的建構式注入";
    this.myDependency1.InstanceName = "myDependency1";
    ...
}
不過,建構函式內的參數僅能夠在建構函式內使用,當結束建構函式執行,呼叫到別的類別方法,就無法再度使用這些建構函式內傳入進來的參數,因此,需要在這個控制器類別內宣告相對應的欄位,用來儲存這些 IoC 容器透過建構函式注入的參數所參考到的執行個體。底下的程式碼將是用來儲存建構函式注入的參數執行個體。
C Sharp / C#
private readonly IMyDependency myDependency1;
private readonly IMyDependency myDependency2;
private readonly IServiceProvider serviceProvider;
private readonly IUnityContainer container;
private readonly IDefaultDependency defaultDependency1;
private readonly IDefaultDependency defaultDependency2;

使用服務定位器 Service Locator 來手動注入實作物件

在某些時候想要自己來透過 IoC 容器取得特定介面的具體實作物件,這個時候就可以使用 服務定位器 service locator pattern 設計模式來做到;在 ASP.NET Core 開發框架中,提供一個 IServiceProvider 介面,只要取得這個介面的具體實作物件,就可以提供一個介面給 IoC 容器,透過 IoC 容器來提供一個具體實作的執行個體。
想要取得 IServiceProvider 介面的具體實作物件可以透過剛剛提到的建構函式注入方法,在該建構函式內提供 IServiceProvider serviceProvider 這個參數宣告,就可以取得 IServiceProvider 介面的具體實作物件。
另外,在 ASP.NET Core 控制器中,可以透過 IServiceProvider contextServiceProvider = HttpContext.RequestServices; 陳述式來取得 IServiceProvider 介面的具體實作物件,另外,也可以透過 IApplicationBuilder 介面中的 ApplicationServices 屬性,一樣是可以取得 IServiceProvider 介面的具體實作物件。
一旦取得了 IServiceProvider 介面的具體實作物件,便可以使用 GetService 或者 GetRequiredService 方法來取得具體實作類別的執行個體,兩者的差異在於當使用前者方法來取得 IoC 容器內的抽象介面之實作物件的時候,若無法找到該介面對應到類別的對應,將會回傳 空值 null;而對於使用後者方法發生同樣的問題,則會拋出例外異常錯誤。
還有在使用 GetService 或者 GetRequiredService 方法的時候,將會發現到使能夠使用非強行別的方法,也就是呼叫這兩個方法的時候,將會回傳一個 object 型別的物件,我們需要自己做型別轉換的工作,因此,建議在這個專案最前面加入這個 using Microsoft.Extensions.DependencyInjection; 命名空間參考,如此當使用這兩個方法的時候,將會看到有泛型多載的強行別方法可以使用,這樣在進行程式設計的時候,許多的一些問題就可以在建置時期發現到設計錯誤的問題,而不用等到執行時期在會看到這些例外異常問題產生,可以大幅增加設計出來程式的可靠度。
然而,許多人對於在開發 ASP.NET Core 專案的時候,並不建議在專案內使用 服務定位器 service locator 功能,因為通稱這樣的設計模式為反模式

使用 Unity 的 IoC 容器用法

對於 ASP.NET Core 開發框架所提供的 IoC 容器,只有提供建構函式方式來注入實作物件,並且對於更多關於相依性注入的應用,並沒有提供;例如:透過屬性方式來注入物件、具名註冊與注入、指定要呼叫的注入建構函式等等。不過,ASP.NET Core 開發框架提供一個擴充機制,讓開發者使用其他好用的 DI 容器程式庫整合到 ASP.NET Core 專案內,使得許多相依性注入的應用與技巧可以在開發專案的時候還使用。

安裝與設定 ASP.NET Core 使用的 Unity IoC 容器

在這裡,將說明如何使用 Unity 這個 DI 容器套件要整合到 ASP.NET Core 專案內的作法,首先,需要安裝 Unity.Microsoft.DependencyInjection 這個 NuGet 套件到專案內,這個範例專案使用現在最新的 2.1.1 版本 。

進行同一個抽象型別對應到多個具體實作型別

接著,需要修正 Startup.cs 檔案內的 Startup 類別;若透過 Visual Studio 2017 建立起一個 ASP.NET Core Web API 類型的專案,在 Startup 類別內將會有個 ConfigureServices 方法,這個方法沒有回傳任何物件,所以,請要修正這個方法,讓 ASP.NET Core 可以使用 Unity 這個 DI 容器。
C Sharp / C#
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
將 ConfigureServices 方法修正如底下程式碼,此時這個 ConfigureServices 方法將會回傳 IServiceProvider 型別的物件。接著 呼叫 Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services) 方法取得 Unity 提供的 ServiceProvider 物件,最後,使用呼叫 ConfigureContainer(Container=(UnityContainer)serviceProvider); 方法,使用 IUnityContainer 實作物件來進行抽象介面與類別的註冊工作。最重要的是,要把剛剛得到的 Unity 提供的 ServiceProvider 回傳回去,這樣,ASP.NET Core 系統就可以與 Unity 這個第三方 DI 容器進行整合了。
C Sharp / C#
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddScoped<IDefaultDependency, MyDefaultClass1>();
    services.AddTransient<IMyDependency, MyClass2>();

    Unity.Microsoft.DependencyInjection.ServiceProvider serviceProvider =
        Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services)
        as Unity.Microsoft.DependencyInjection.ServiceProvider;
    ConfigureContainer(Container=(UnityContainer)serviceProvider);
    return serviceProvider;
}

使用具名注入來注入具體實作物件

現在來看看 ConfigureContainer 這個方法做了哪些事情,這個方法是需要另外新建立起來的,當建立好 Unity 容器之後,也就是 IUnityContainer 這個抽象介面的物件,需要透過 IUnityContainer 使用 Unity 容器的用法,進行抽象介面與類別的註冊。在這裡,希望能夠在 ASP.NET Core 專案內,針對 IMyDependency 這個介面要去註冊同時可以對應到 MyClass1 與 MyClass2 這兩個類別,不過,希望設定 MyClass2 為預設注入的類別,也就是說,在進行建構式注入的過程,會預設使用 MyClass2 類別來產生出執行個體。
因此,在這裡需要使用 Unity 的具名註冊的功能,也就是說,需要透過 Unity 容器提供的註冊方法,進行 介面 IMyDependency, 類別 MyClass1 的註冊,但是會提供一個字串到 RetisterType 方法內;所以,日後想要注入 IMyDependency 介面的具體實作物件,但是其為 MyClass1 類型的執行個體,需要 IUnityContainer 的實作物件以及使用前面說明過的 服務定位器 Service Locator 功能,將這個具名字串傳送到解析 Resolve 方法內。
C Sharp / C#
 public void ConfigureContainer(IUnityContainer container)
 {
     // Could be used to register more types
     container.RegisterType<IMyDependency, MyClass1>("MyClass1");
 }
但是,在類別中想要使用具名解析功能的時候,要如何取得 IUnityContainer 介面的實作物件呢?這裡可以透過控制器的建構函式,使用建構式注入 IUnityContainer 的方式,這樣就可以在該控制器類別中使用 IUnityContainer 所提供的各項功能;另外,在這個範例中,當在 ConfigureServices 方法內,使用 Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices 函式產生出一個 IUnityContainer 實作物件之後,之後有使用這樣的 Container=(UnityContainer)serviceProvider 陳述式,將 IUnityContainer 這個具體實作物件,設定到 Startup 類別中的 靜態屬性 Container 上,這樣,就可以在任何類別中,直接透過 Startup.Container 這個靜態屬性來進行相依性注入的解析動作了。
在 ValuesController 類別內的 Get() 方法,可以使用 Startup.Container.Resolve<IMyDependency>("MyClass1"); 表示式,透過 Unity 容器來進行具名註冊,這樣的表示式呼叫完成之後,將會得到一個 MyClass1 類別的執行個體,反過來說,若呼叫 Resolve 方法且沒有傳遞任何字串引數,則將會得到 MyClass2 類別的執行個體。