.NET 6 預覽版 7 釋出——最後一個預覽版

精緻碼農發表於2021-08-11

原文:bit.ly/2VJxjxQ
作者:Richard
翻譯:精緻碼農-王亮
說明:文中有大量的超連結,這些連結在公眾號文章中被自動剔除,一部分包含超連結列表的小段落被我刪減了,如果你對此感興趣,請參考閱讀原文。

我們很高興地釋出了 .NET 6 預覽版 7。這是我們進入(兩個)候選釋出版(RC)之前的最後一個預覽版。在我們放慢釋出速度之前,團隊一直在螢窗雪案,以完成最後一組功能。在這個版本中,你將看到各功能的最後一次拋光,一次到位地整合整個版本的大型功能。從此時起,團隊將專注於使所有的功能達到統一的(高)質量,以便 .NET 6 為你的生產工作做好準備。

關於生產工作的話題,值得提醒大家的是,.NET 官網[1]和 Bing.com 從預覽版 1 開始就一直執行在 .NET 6 上。我們正在與不同的團隊(微軟和其他公司)商談有關進入生產的 .NET 6 RC 版本。如果你對此感興趣,並希望得到相關的指導,請聯絡 dotnet@microsoft.com。我們始終很樂意與早期採用者交流。

你可以在這下載[2] Linux、macOS 和 Windows 的 .NET 6 預覽版 7。

- 安裝程式和二進位制檔案
  https://dotnet.microsoft.com/download/dotnet/6.0
- 容器映象
  https://hub.docker.com/_/microsoft-dotnet
- Linux 包
  https://github.com/dotnet/core/blob/main/release-notes/6.0/install-linux.md
- 釋出說明
  https://github.com/dotnet/core/blob/main/release-notes/6.0/README.md
- API 差異
  https://github.com/dotnet/core/tree/main/release-notes/6.0/preview/api-diff/preview7
- 已知問題
  https://github.com/dotnet/core/blob/main/release-notes/6.0/known-issues.md
- GitHub issue 跟蹤
  https://github.com/dotnet/core/issues/6554

請參閱 .NET MAUI[3] 和 ASP.NET Core[4],瞭解更多關於客戶端和 Web 應用場景的新內容。

.NET 6 預覽版 7 已經在 Visual Studio 2022 預覽版 3 中測試通過並得到支援。Visual Studio 2022 使你能夠利用為 .NET 6 開發的 Visual Studio 工具,如 .NET MAUI 的開發、C# 應用程式的 Hot Reload、WebForms 的 Web Live,以及 IDE 體驗中的其他效能改進。Visual Studio Code 也支援 .NET 6。

請檢視我們新的對話帖[5],瞭解工程師之間關於最新的 .NET 功能的深入討論。我們還發表了關於 C# 10 中的字串插值和 .NET 6 中的預覽功能 - 泛型 Math[6]

.NET SDK:現代化的 C# 專案模板

我們更新了 .NET SDK 的模板,以使用最新的 C# 語言特性和模式。我們已經有一段時間沒有在新的語言特性方面重新審視這些模板了。現在是時候了,我們將確保模板在未來使用新的功能。

以下是新模板中使用的語言特性:

  • 頂層語句
  • async Main
  • 全域性 using 指令(通過 SDK 驅動的預設值)
  • File-scoped 名稱空間
  • 目標型別 new 表示式
  • 可空(Nullable)引用型別

你可能會問,為什麼我們要通過模板啟用某些功能,而不是在專案以 .NET 6 為 Target 時預設啟用這些功能。儘管我們可以要求你在升級應用程式到新版本的 .NET 時做一些工作,作為改善平臺預設行為的交換條件,這使我們能夠改進產品,而不會使專案檔案隨著時間的推移而變得複雜。然而,有些功能對於這種模式來說可能是相當具有破壞性的,比如可空(Nullable)的引用型別。無論是在什麼時候,我們都不想把這些功能與升級體驗聯絡在一起,而是想把這個選擇權留給你。模板是一個風險更低的支點,在那裡我們能夠為新的程式碼設定新的“好的預設模型”,而不會產生那麼多下游的後果。通過專案模板啟用這些功能,我們得到了兩全其美的結果:新程式碼開始時啟用了這些功能,但現有的程式碼在你升級時不會受到影響。

控制檯模板

控制檯模板變化最大,通過頂層語句和全域性引用指令,它現在是一個單行程式碼:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

而以前的同一模板的 .NET 5 版本是這樣的:

using System;

namespace Company.ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

控制檯模板的專案檔案也發生了變化,啟用了可空(Nullable)引用型別的功能,例如:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

其他模板也可以實現可空(Nullable)引用型別、隱式全域性引用和 File-scoped 名稱空間,包括 ASP.NET Core 和 類庫。

ASP.NET Web 模板

Web 模板也同樣減少了程式碼行數,使用同樣的功能:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", () => "Hello World!");

app.Run();

ASP.NET MVC 模板

MVC 模板的結構也類似。在這種情況下,我們將 Program.csStartup.cs 合併為一個檔案(Program.cs),形成了進一步的簡化:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

模板相容性

關於使用新模板的相容性問題,請參見以下內容:

  • 模板中的 C# 程式碼不被早期的 .NET 版本所支援[7]
  • 隱式名稱空間引入[8]

類庫:反射 API 的可空性資訊

可空引用型別[9]是編寫可靠程式碼的一個重要特徵。它在編寫程式碼時非常有用,但在檢查程式碼時卻沒有(直到現在)。新的反射 API[10] 使你能夠確定一個給定方法的引數和返回值的可空性。這些新的 API 對於基於反射的工具和序列化來說是至關重要的,比如說:

就上下文而言,我們在 .NET 5 中為 .NET 庫新增了可空標註[11](在.NET 6 中完成),並且正在為 ASP.NET Core 的這個版本做同樣的工作。我們也看到開發者在他們的專案中採用了可空性(Nullability)[12]

可空性(Nullability)資訊存在於使用自定義屬性的後設資料中[13]。原則上,任何人都已經可以讀取自定義屬性,然而,這並不理想,因為編碼的消耗非同小可。

下面的例子演示了在幾個不同的場景中使用新的 API。

獲取頂層的可空性資訊

想象一下,你正在實現一個序列化器。使用這些新的 API,序列化器可以檢查一個給定的屬性是否可以被設定為 null

private NullabilityInfoContext _nullabilityContext = new NullabilityInfoContext();

private void DeserializePropertyValue(PropertyInfo p, object instance, object? value)
{
    if (value is null)
    {
        var nullabilityInfo = _nullabilityContext.Create(p);
        if (nullabilityInfo.WriteState is not NullabilityState.Nullable)
        {
            throw new MySerializerException($"Property '{p.GetType().Name}.{p.Name}'' cannot be set to null.");
        }
    }

    p.SetValue(instance, value);
}

獲取巢狀的可空性資訊

可空性對可以持有其他物件的物件有特殊處理,比如陣列和元組。例如,你可以指定一個陣列物件(作為一個變數,或者作為一個型別成員簽名的一部分)必須是非空的,但是元素可以是空的,或者相反。這種額外的特殊性是可以通過新的反射 API 來檢查的,例如:

class Data
{
    public string?[] ArrayField;
    public (string?, object) TupleField;
}
private void Print()
{
    Type type = typeof(Data);
    FieldInfo arrayField = type.GetField("ArrayField");
    FieldInfo tupleField = type.GetField("TupleField");

    NullabilityInfoContext context = new ();

    NullabilityInfo arrayInfo = context.Create(arrayField);
    Console.WriteLine(arrayInfo.ReadState);        // NotNull
    Console.WriteLine(arrayInfo.Element.State);    // Nullable

    NullabilityInfo tupleInfo = context.Create(tupleField);
    Console.WriteLine(tupleInfo.ReadState);                      // NotNull
    Console.WriteLine(tupleInfo.GenericTypeArguments [0].State); // Nullable
    Console.WriteLine(tupleInfo.GenericTypeArguments [1].State); // NotNull
}

類庫:ZipFile 遵循 Unix 檔案許可權

System.IO.Compression.ZipFile 類現在可以在建立過程中捕獲 Unix 檔案許可權,並在類似 Unix 的作業系統上提取壓縮檔案時設定檔案許可權。這一變化允許可執行檔案在壓縮包中被迴圈使用,這意味著你不再需要修改檔案許可權來使檔案在解壓縮包後可執行。它也同樣遵循 usergroupother 讀/寫許可權。

如果一個壓縮包不包含檔案許可權(因為它是在 Windows 上建立的,或者使用了一個沒有捕獲許可權的工具,比如早期的 .NET 版本),那麼解壓縮的檔案就會得到預設的檔案許可權,就像其他新建立的檔案一樣。

Unix 的檔案許可權也適用於其他壓縮工具,包括:

  • Info-ZIP
  • 7-Zip

早期 .NET 7 功能預覽:泛型 Math

對於 .NET 6,我們已經建立了將 API 標記為“預覽”的能力[14]。這種新方法將使我們能夠在多個主要版本中提供和發展預覽功能。為了使用預覽 API,專案需要明確選擇使用的預覽功能。如果你在沒有明確選擇的情況下使用預覽功能,從 .NET 6 RC1 開始,你會看到帶有可操作資訊的構建錯誤。預覽功能預計將在以後的版本中發生變化,可能會有破壞性的變化。這就是為什麼他們要選擇加入。

我們在 .NET 6 中預覽的這些功能之一是靜態抽象介面成員。這些允許你在介面中定義靜態的抽象方法(包括操作符)。例如,現在可以實現代數泛型方法。對於一些人來說,這個功能將是我們今年交付的絕對突出的改進。這也許是自 Span<T> 以來最重要的新型別系統特性。

下面的例子是一個 IEnumerable<T>,由於 T 被限制為 INumber<T>,可能是一個 INumber<int>,所以能夠對所有的數值進行求和。

public static T Sum<T>(IEnumerable<T> values)
    where T : INumber<T>
{
    T result = T.Zero;

    foreach (var value in values)
    {
        result += value;
    }

    return result;
}

這是因為 INumber<T> 定義了各種(靜態)操作符過載,必須由介面實現者來滿足。IAdditionOperators 也許是最容易理解的新介面,INumber<T> 本身就是派生自這個介面。

這都是由一個新的功能提供的,它允許在介面中宣告靜態抽象成員。這使得介面可以公開運算子和其他靜態方法,比如 ParseCreate,並且這些方法可以由派生型別實現。更多細節請參見我們的相關博文[15]

所有提到的功能都是 .NET 6 的預覽版,不支援在生產中使用。我們將感謝您在使用中提供反饋。我們打算在 .NET 7 中繼續發展和改進泛型 Math 功能以及支援它們的執行時和 C# 功能。我們希望對當前的體驗進行突破性的改變,這也是為什麼新的 API 被標記為“預覽”的部分原因。

類庫:NativeMemory API

我們增加了新的本地記憶體分配 API[16],通過 System.Runtime.InteropServices.NativeMemory 公開。這些 API 相當於 C 語言中的 mallocfreerealloccalloc API,還包括用於進行對齊分配的 API。

你可能想知道如何看待這些 API。首先,它們是低階別的 API,是為低階別的程式碼和演算法準備的。應用程式開發人員很少會用到這些。另一種思考這些 API 的方式類似於平臺內部的 API,它們是用於 CPU 指令的低階別 .NET API。這些 API 是類似的,但它是為記憶體相關的操作暴露的低階別的 API。

類庫:System.Text.Json 序列化通知

System.Text.Json 序列化器現在將通知作為(反)序列化操作的一部分公開。它們對於預設值和驗證非常有用。要使用它們,請在 System.Text.Json.Serialization 名稱空間中實現一個或多個介面 IJsonOnDeserializedIJsonOnDeserializingIJsonOnSerialized 或 IJsonOnSerializing`。

這裡有一個例子,在 JsonSerializer.Serialize()JsonSerializer.Deserialize() 中都進行驗證,以確保 FirstName 屬性不是 null

public class Person : IJsonOnDeserialized, IJsonOnSerializing
{
    public string FirstName{ get; set; }

    void IJsonOnDeserialized.OnDeserialized() => Validate(); // Call after deserialization
    void IJsonOnSerializing.OnSerializing() => Validate(); // Call before serialization

    private void Validate()
    {
        if (FirstName is null)
        {
            throw new InvalidOperationException("The 'FirstName' property cannot be 'null'.");
        }
    }
}

以前,你需要實現一個自定義轉換器來實現這一功能。

類庫:System.Text.Json 序列化屬性排序

我們使用 System.Text.Json.Serialization.JsonPropertyOrderAttribute 特性增加了控制屬性序列化順序的能力,用一個整數指定了順序,較小的整數先被序列化;沒有該特性的屬性有一個預設的排序值 0

這裡有一個例子,指定 JSON 應該按照 Id, City, FirstName, LastName 的順序進行序列化:

public class Person
{
    public string City { get; set; } // No order defined (has the default ordering value of 0)

    [JsonPropertyOrder(1)] // Serialize after other properties that have default ordering
    public string FirstName { get; set; }

    [JsonPropertyOrder(2)] // Serialize after FirstName
    public string LastName { get; set; }

    [JsonPropertyOrder(-1)] // Serialize before other properties that have default ordering
    public int Id { get; set; }
}

以前,序列化順序是由反射順序決定的,而反射順序既不是確定的,也不會導致特定的預期順序。

類庫:System.Text.Json.Utf8JsonWriter

在用 Utf8JsonWriter 編寫 JSON payloads 時,有時你需要嵌入“原始”JSON。

比如:

  • 我有一個設計好的位元組序列,如下例所示。
  • 我有一個 blob,我認為它代表 JSON 內容,我想把它包起來,我需要確保包和它的內部保持良好的格式。
JsonWriterOptions writerOptions = new() { WriteIndented = true, };

using MemoryStream ms = new();
using UtfJsonWriter writer = new(ms, writerOptions);

writer.WriteStartObject();
writer.WriteString("dataType", "CalculationResults");

writer.WriteStartArray("data");

foreach (CalculationResult result in results)
{
    writer.WriteStartObject();
    writer.WriteString("measurement", result.Measurement);

    writer.WritePropertyName("value");
    // Write raw JSON numeric value using FormatNumberValue (not defined in the example)
    byte[] formattedValue = FormatNumberValue(result.Value);
    writer.WriteRawValue(formattedValue, skipValidation: true);

    writer.WriteEndObject();
}

writer.WriteEndArray();
writer.WriteEndObject();

以下是對上述程式碼--特別是FormatNumberValue--的描述。為了提高效能,System.Text.Json 在數字為整數時省略了小數點/值,如 1.0。其理由是,寫的位元組數越少越好,有利於提高效能。在某些情況下,保留小數點可能很重要,因為消費者將沒有小數點的數字視為整數,否則視為浮點數。這種新的“原始值”模型允許你在任何需要的地方擁有這種程度的控制。

類庫:JsonSerializer 同步流過載

我們為 JsonSerializer 新增了新的同步 API[17],用於將 JSON 資料序列化和反序列化到一個流。你可以在下面的例子中看到這個演示。

using MemoryStream ms = GetMyStream();
MyPoco poco = JsonSerializer.Deserialize<MyPoco>(ms);

這些新的同步 API 包括與新的 System.Text.Json source generator[18] 相容和可用的過載,通過接受 JsonTypeInfo<T>JsonSerializerContext 例項。

類庫:System.Diagnostics Propagators

在過去的幾年裡,我們一直在改進對 OpenTelemetry[19] 的支援。實現該支援的一個關鍵點是確保所有需要參與遙測生產的元件以正確的格式輸出到網路頭。要做到這一點真的很難,特別是隨著 OpenTelemetry 規範的變化。OpenTelemetry 定義了傳播(propagation)[20]的概念來幫助解決這種情況。我們正在採用傳播的方式來實現頭的定製的一般模型。

關於更廣泛的概念背景:

  • OpenTelemetry 規範 - 分散式跟蹤資料結構的記憶體表示。
  • OpenTelemetry Span - 追蹤構建塊,在 .NET 中由 System.Diagnostics.Activity 表示。
  • W3C TraceContext - 關於如何通過眾所周知的 HTTP 頭傳播這些分散式跟蹤資料結構的規範。

下面的程式碼演示了使用傳播的一般方法:

DistributedContextPropagator propagator = DistributedContextPropagator.Current;
propagator.Inject(activity, carrier, (object theCarrier, string fieldName, string value) =>
{
   // Extract the context from the activity then inject it to the carrier.
});

你也可以選擇使用不同的傳播器(propagator):

// Set the current propagation behavior to not transmit any distributed context information in outbound network messages.
DistributedContextPropagator.Current = DistributedContextPropagator.CreateNoOutputPropagator();

DistributedContextPropagator 抽象類決定了分散式上下文資訊在網路傳輸時是否以及如何被編碼和解碼。編碼可以通過任何支援字串鍵/值對的網路協議進行傳輸。DistributedContextPropagator 以字串鍵/值對的形式向載體注入數值並從載體中提取數值。通過新增對傳播者的支援,我們實現了兩件事。

  • 你不再需要使用 W3C 的 TraceContext 標頭檔案。你可以編寫一個自定義的傳播器(甚至用你自己的標頭檔案名稱),而不需要 HttpClient、ASP.NET Core 等庫對這種自定義格式有預先的瞭解。
  • 如果你實現了一個帶有自定義傳輸的庫(如訊息佇列),只要你支援傳送和接收文字對映(如 Dictionary<string, string>),你現在可以支援各種格式。

大多數應用程式程式碼不需要直接使用這個功能,然而,如果你使用 OpenTelemetry,你很可能會在呼叫棧中看到它。一些庫的程式碼如果關心跟蹤和因果關係,可能會需要使用這個模型。

類庫:加密操作呼叫模式簡化

.NET 的加密和解密部件是圍繞著流設計的,沒有真正的概念來定義什麼時候有效載荷已經在記憶體中(already in memory)。SymmetricAlgorithm 上新的 Encrypt-Decrypt- 方法加速了 already in memory 的進展,目的是為呼叫者和程式碼審查者提供清晰的資訊。此外,它們還支援從 span 中讀取和寫入。

新的簡化方法為使用加密 API 提供了一個直接的方法:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;

        return aes.DecryptCbc(ciphertext, iv);
    }
}

在新的 Encrypt-Decrypt- 方法中,只使用 SymmetricAlgorithm 例項中的 Key 屬性。新的 DecryptCbc 方法支援選擇填充演算法,但是 PKCS#7 經常與 CBC 一起使用,所以它是一個預設引數。如果你喜歡這種清晰的感覺,就指定它吧:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;

        return aes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7);
    }
}

你可以看到,現有的模式--使用 .NET 5--明顯需要更多的管道來實現同樣的結果:

private static byte[] Decrypt(byte[] key, byte[] iv, byte[] ciphertext)
{
    using (Aes aes = Aes.Create())
    {
        aes.Key = key;
        aes.IV = iv;

        // These are the defaults, but let's set them anyways.
        aes.Padding = PaddingMode.PKCS7;
        aes.Mode = CipherMode.CBC;

        using (MemoryStream destination = new MemoryStream())
        using (ICryptoTransform transform = aes.CreateDecryptor())
        using (CryptoStream cryptoStream = new CryptoStream(destination, transform, CryptoStreamMode.Write))
        {
            cryptoStream.Write(ciphertext, 0, ciphertext.Length);
            cryptoStream.FlushFinalBlock();
            return destination.ToArray();
        }
    }
}

執行時:支援所有平臺和架構的 W^X

執行時現在有一種模式,它不建立或使用任何同時可寫和可執行的記憶體頁。所有可執行的記憶體都被對映為只讀不執行。這項功能在該版本的早期僅在 macOS 上啟用--針對 Apple Silicon。在 Apple Silicon 機器上,禁止同時進行可寫和可執行的記憶體對映。

這一功能現在在所有其他平臺上被啟用並支援。在這些平臺上,可執行程式碼的生成/修改是通過單獨的讀寫記憶體對映完成的,這對 JIT 程式碼和執行時生成的輔助程式都是如此。這些對映是在與可執行程式碼地址不同的虛擬記憶體地址上建立的,並且只在進行寫入時存在非常短暫的時間。例如,JIT 現在生成程式碼到一個從頭開始的緩衝區,在整個方法被 jitted 後,使用一個記憶體拷貝函式呼叫將其複製到可執行記憶體中。而可寫對映的壽命只跨越了記憶體拷貝的時間。

這個新功能可以通過設定環境變數 DOTNET_EnableWriteXorExecute 為 1 來啟用。這個功能在 .NET 6 中是可選的,因為它有一個啟動時的退步(除了在 Apple Silicon 上)。在我們的 ASP.NET 基準測試中,當用 Ready To Run(R2R)編譯時,退步了 ~10%。然而,在啟用和未啟用該功能的情況下,測得的穩態效能是一樣的。對於啟動效能並不重要的應用程式,我們建議啟用該功能,因為它能提高安全性。我們打算作為 .NET 7 的一部分解決效能退步問題,屆時預設啟用該功能。

結束

我們認為,我們已經到了新功能和改進已經完成的釋出點。為團隊點贊!

我們繼續期待你們的反饋。我們將把 .NET 6 的其餘部分放在完善(功能和效能)和新功能中發現的錯誤上。在大多數情況下,功能改進需要等到 .NET 7。請分享你的任何反饋,我們將很高興對其進行分類。

感謝所有為 .NET 6 做出貢獻的人,使其成為另一個偉大的版本。

感謝你成為一名 .NET 開發者。

文中相關連結:

[1].https://dotnet.microsoft.com/
[2].https://dotnet.microsoft.com/download/dotnet/6.0
[3].https://devblogs.microsoft.com/dotnet/announcing-net-maui-preview-7/
[4].https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-7/
[5].https://devblogs.microsoft.com/dotnet/category/conversations/
[6].https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/
[7].https://docs.microsoft.com/dotnet/core/compatibility/sdk/6.0/csharp-template-code
[8].https://docs.microsoft.com/dotnet/core/compatibility/sdk/6.0/implicit-namespaces
[9].https://docs.microsoft.com/dotnet/csharp/nullable-references
[10].https://github.com/dotnet/runtime/issues/29723
[11].https://twitter.com/JeffHandley/status/1424846146850131968
[12].https://github.com/jellyfin/jellyfin/blob/c07e83fdf87e61f30e4cca4e458113ac315918ae/Directory.Build.props#L5
[13].https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
[14].https://github.com/dotnet/designs/blob/main/accepted/2021/preview-features/preview-features.md
[15].https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/
[16].https://github.com/dotnet/runtime/pull/54006
[17].https://github.com/dotnet/runtime/issues/1574
[18].https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/
[19].https://devblogs.microsoft.com/dotnet/opentelemetry-net-reaches-v1-0/
[20].https://opentelemetry.lightstep.com/core-concepts/context-propagation/

相關文章