解開 .NET Core 跨平台的黑魔法

.NET Core 能跨平台看似很神奇,事實上底層所使用的技術為 Type Forwarding,說穿了就是 Proxy Pattern 的應用。

Version


macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
JetBrains Rider 2017.3.1
Visual Studio 2017 15.6.2

Definition


Client 在不改變 assembly reference 狀態下,就可使用其他 assembly 所提供的 class。

ype00

.NET App 會直接與 Assembly A 耦合,僅能使用 Assembly AMyClass

ype00

.NET App 依然只與 Assembly A 耦合,但卻能夠透過 Type Forwarding 使用 Assembly BMyClass

實戰 Type Forwarding


建立 ClassLibrary1

Class1.cs

1
2
3
4
5
6
7
8
9
10
namespace ClassLibrary1
{
public class Class1
{
public int Sum(int x, int y)
{

return x + y;
}
}
}

ClassLibrary1 assembly 建立 Class1.Sum()

建立 ConsoleApp

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using ClassLibrary1;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var object1 = new Class1();
var result = object1.Sum(1, 1);
Console.WriteLine(result);
}
}
}

ConsoleApp 使用 ClassLibrary1Class1.Sum()

ype00

  1. 目前只有 ClassLibrary1ConsoleApp
  2. 執行結果為 2
  3. ConsoleApp 的 project reference 只有 ClassLibrary1

建立 ClassLibrary2

Class1.cs

1
2
3
4
5
6
7
8
9
10
namespace ClassLibrary1
{
public class Class1
{
public int Sum(int x, int y)
{

return x + y;
}
}
}

ClassLibrary1Class1.cs 全部程式碼搬到 ClassLibrary2

ClassLibrary1 使用 Type Forwarding

Class1.cs

1
2
3
using System.Runtime.CompilerServices;

[assembly:TypeForwardedTo(typeof(ClassLibrary1.Class1))]

因為 ClassLibrary1Class1.cs 程式碼已經搬到 ClassLibrary2,將原本 ClassLibrary1Class1.cs 加上 Type Forwarding。

ConsoleApp 使用 ClassLibrary1 assembly 的 ClassLibrary1.Class1 時,會自動 Type Forwarding 到 ClassLibrary2 assembly 的 ClassLibrary1.Class1

Rebuild 整個 solution 的 3 個 project 並執行。

ype00

  1. 目前有 ClassLibrary1ClassLibrary2ConsoleApp
  2. 執行結果依然為 2
  3. 原本 ConsoleApp 的 project reference 只有 ClassLibrary1,因為 Type Forwarding,所以現在也有了 ClassLibrary2

我們並沒有對 ConsoleAppClassLibrary2 加入 project reference,而是因為 Type Forwarding

觀察 netstandard Assembly


Metapackage 與 .NET Standard 談了這麼多,是否感覺很抽象呢 ? 讓我們以反組譯 netstandard Assembly 來理解其中的黑魔法。

使用 Visual Studio 2017 開啟 solution。

ype00

View -> Object Browser 開啟 Object Browser,可以讓我們看到整個 solution 所用到的 assembly。

  1. 展開 ClassLibrary1,看不到任何 namespace 與 class,因為我們已經在 ClassLibrary1 使用 Type Forwarding。
  2. 展開 ClassLibrary2,我們看到了 ClassLibrary1 namespace 與 Class1 class。

若 assembly 使用了 Type Forwarding,使用 Object Browser 將看不到該 assembly 任何實作的 namespace 與 class

ype00

  1. 點擊 mscorlibnetstandard 兩個 assembly ,亦發現沒有任何實作的 namespace 與 class,其中 mscorlib 就是 Microsoft.NETCore.App,而 netstandard 就是 NETStandard.Library
  2. 顯示 netstandard assembly 的實際路徑

由於 mscorlibnetstandard 都沒有任何實作,根據 ClassLibrary1 的經驗推測,很可能也是用了 Type Forwarding

ype00

使用 ILSpy 反組譯 netstandard assembly,我們可以發現真的沒有任何實作,但大量使用了 Type Forwarding。

在傳統 .NET Framework 世界裡,我們是直接參考 .NET Framework 內某個 assembly 的 class,也因此與 Windows 平台的 assembly 的 class 耦合。

但在 .NET Core 世界裡,我們改參考 .NET Standard,再由 .NET Standard 透過 Type Forwarding 方式參考實際 host 的 assembly 的 class,若 host 是 macOS,則參考 macOS 上的 assembly 的 class,若 host 是 Linux,則參考 Linux 上的 assembly 的 class。

因為 client 只與有 Type Forwarding 的 .NET Standard 耦合,而沒與特定平台的 class 耦合,所以 .NET Core 能達到跨平台,在不同 host os 下,不用編譯就可以自動找到正確的 class

Proxy Pattern


Proxy Pattern

Client 為了避免與實際物件耦合,改透過相同 interface 的 白手套 proxy 耦合,再由 proxy 負責與實際物件溝通

ype00

Client 實際上要使用 RealSubject,但為了將 RealSubjectClient 解耦合,Client 只與相同 interface 的 Proxy 耦合,再由白手套 Proxy 使用 RealSubject

.NET Standard

ype00

Client 實際上要使用各平台的 .NET Core SDK,但為了將各平台的 .NET Core SDKClient 解耦合,Client 只與一樣遵守 .NET Standard 的 netstandard 耦合,再由白手套 netstandard 透過 Type Forwarding 使用各平台的 .NET Core SDK

Conclusion


  • Metapackage 與 .NET Standard 不是什麼黑魔法,只是一個使用 Type Forwarding 的 DLL
  • 我們只要跟 .NET Standard 耦合即可,它會使用 Type Forwarding 幫我們使用各平台實際的 class

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Microsoft Docs, Type Fowarding in the Common Language Runtime

2018-03-22