共用方式為


建立自訂 .NET Aspireclient 整合

本文是 建立自定義 .NET.NET Aspire 主機整合 一文的延續。 它會引導您建立使用 MailKit 傳送電子郵件的 .NET Aspireclient 整合。 接著,此整合會新增至您先前建置的電子報應用程式。 上一個範例省略了建立 client 整合,而是依賴現有的 .NETSmtpClient。 最好使用 MailKit 的 SmtpClient 而不是官方的 .NETSmtpClient 來傳送電子郵件,因為它更現代化,並支援更多功能和通訊協定。 如需詳細資訊,請參閱 .NET SmtpClient:備註

先決條件

如果您按照步驟操作,您應該有一個電子報應用程式,這來源於 建立自定義 .NET.NET Aspire 主機整合 文章中的步驟。

提示

本文以現有 .NET.NET Aspire 整合為靈感,並根據團隊的官方指引。 在某些地方,指導方針有所不同,理解這些差異背後的原因是重要的。 如需詳細資訊,請參閱 .NET.NET Aspire 整合需求

建立整合函式庫

.NET .NET Aspire 整合 會以 NuGet 套件的形式傳遞,但在此範例中,發佈 NuGet 套件超出本文的範圍。 相反地,您會建立包含整合的類別庫專案,並將其參考為專案。 .NET Aspire 整合套件的目的是包裝 client 函式庫,例如 MailKit,並提供生產環境的遙測、健康檢查、設定的靈活性和測試的可能性。 讓我們從建立新的類別庫項目開始。

  1. 在與上一篇文章中 MailDevResource.sln 相同的目錄中,建立名為 MailKit.Client 的新類別庫專案。

    dotnet new classlib -o MailKit.Client
    
  2. 將專案新增至方案。

    dotnet sln ./MailDevResource.sln add MailKit.Client/MailKit.Client.csproj
    

下一個步驟是新增整合所依賴的所有 NuGet 套件。 與其讓您從 .NET CLI 逐一新增每個套件,可能更容易將下列 XML 複製並貼到 MailKit 中的Client.csproj 檔案。

<ItemGroup>
  <PackageReference Include="MailKit" Version="4.9.0" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
  <PackageReference Include="Microsoft.Extensions.Resilience" Version="9.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
  <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
  <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
</ItemGroup>

定義整合設定

每當您建立 .NET Aspire 整合時,最好瞭解您要對應到的 client 程式庫。 使用MailKit時,您必須瞭解連線到簡單郵件傳輸通訊協定 (SMTP) server所需的組態設定。 但還需了解程式庫是否支援 健康情況檢查追蹤計量。 MailKit 透過的 類別,支援 追蹤計量。 新增 健康檢查時,您應盡可能使用任何已建立或現有的健康檢查。 否則,您可能會考慮在整合過程中實作自己的方案。 在名為 MailKitClientSettings.cs的檔案中,將下列程式代碼新增至 MailKit.Client 專案:

using System.Data.Common;

namespace MailKit.Client;

/// <summary>
/// Provides the client configuration settings for connecting MailKit to an SMTP server.
/// </summary>
public sealed class MailKitClientSettings
{
    internal const string DefaultConfigSectionName = "MailKit:Client";

    /// <summary>
    /// Gets or sets the SMTP server <see cref="Uri"/>.
    /// </summary>
    /// <value>
    /// The default value is <see langword="null"/>.
    /// </value>
    public Uri? Endpoint { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableHealthChecks { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableTracing { get; set; }

    /// <summary>
    /// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are disabled or not.
    /// </summary>
    /// <value>
    /// The default value is <see langword="false"/>.
    /// </value>
    public bool DisableMetrics { get; set; }

    internal void ParseConnectionString(string? connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new InvalidOperationException($"""
                    ConnectionString is missing.
                    It should be provided in 'ConnectionStrings:<connectionName>'
                    or '{DefaultConfigSectionName}:Endpoint' key.'
                    configuration section.
                    """);
        }

        if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
        {
            Endpoint = uri;
        }
        else
        {
            var builder = new DbConnectionStringBuilder
            {
                ConnectionString = connectionString
            };
            
            if (builder.TryGetValue("Endpoint", out var endpoint) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') is missing.
                        """);
            }

            if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out uri) is false)
            {
                throw new InvalidOperationException($"""
                        The 'ConnectionStrings:<connectionName>' (or 'Endpoint' key in
                        '{DefaultConfigSectionName}') isn't a valid URI.
                        """);
            }

            Endpoint = uri;
        }
    }
}

上述程式碼定義 MailKitClientSettings 類別,使用下列方式:

  • Endpoint 屬性,表示 SMTP server的連接字串。
  • DisableHealthChecks 屬性,用以啟用或停用健康檢查。
  • DisableTracing 屬性,決定是否啟用追蹤。
  • DisableMetrics 屬性,決定是否啟用計量。

剖析連接字串邏輯

settings 類別也包含 ParseConnectionString 方法,可將連接字串剖析為有效的 Uri。 組態預期會以以下格式提供:

  • ConnectionStrings:<connectionName>:SMTP server的連接字串。
  • MailKit:Client:ConnectionString:SMTP server的連接字串。

如果這兩個值皆未提供,則會引發例外狀況。

公開 client 功能

.NET Aspire 整合的目標是透過依賴注入將基礎 client 函式庫公開給使用者。 使用MailKit和此範例時,SmtpClient 類別就是您想要公開的內容。 您不會包裝任何功能,而是將組態設定對應至 SmtpClient 類別。 對於整合來說,暴露標準和含鍵的服務註冊是很常見的。 當服務只有一個實例時,就會使用標準註冊,而當有多個服務實例時,會使用索引鍵服務註冊。 有時候,若要建立相同類型的多個註冊,可以使用工廠模式。 在名為 MailKitClientFactory.cs的檔案中,將下列程式代碼新增至 MailKit.Client 專案:

using MailKit.Net.Smtp;

namespace MailKit.Client;

/// <summary>
/// A factory for creating <see cref="ISmtpClient"/> instances
/// given a <paramref name="smtpUri"/> (and optional <paramref name="credentials"/>).
/// </summary>
/// <param name="settings">
/// The <see cref="MailKitClientSettings"/> settings for the SMTP server
/// </param>
public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    private SmtpClient? _client;

    /// <summary>
    /// Gets an <see cref="ISmtpClient"/> instance in the connected state
    /// (and that's been authenticated if configured).
    /// </summary>
    /// <param name="cancellationToken">Used to abort client creation and connection.</param>
    /// <returns>A connected (and authenticated) <see cref="ISmtpClient"/> instance.</returns>
    /// <remarks>
    /// Since both the connection and authentication are considered expensive operations,
    /// the <see cref="ISmtpClient"/> returned is intended to be used for the duration of a request
    /// (registered as 'Scoped') and is automatically disposed of.
    /// </remarks>
    public async Task<ISmtpClient> GetSmtpClientAsync(
        CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);

        try
        {
            if (_client is null)
            {
                _client = new SmtpClient();

                await _client.ConnectAsync(settings.Endpoint, cancellationToken)
                             .ConfigureAwait(false);
            }
        }
        finally
        {
            _semaphore.Release();
        }       

        return _client;
    }

    public void Dispose()
    {
        _client?.Dispose();
        _semaphore.Dispose();
    }
}

MailKitClientFactory 類別是一個處理站,會根據組態設定建立 ISmtpClient 實例。 它負責返回一個 ISmtpClient 實作,該實作與已設定的 SMTP server保持活躍連線。 接下來,您需要公開功能以便讓使用者能在相依注入容器中註冊此工廠。 在名為 MailKitExtensions.cs的檔案中,將下列程式代碼新增至 MailKit.Client 專案:

using MailKit;
using MailKit.Client;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Provides extension methods for registering a <see cref="SmtpClient"/> as a
/// scoped-lifetime service in the services provided by the <see cref="IHostApplicationBuilder"/>.
/// </summary>
public static class MailKitExtensions
{
    /// <summary>
    /// Registers 'Scoped' <see cref="MailKitClientFactory" /> for creating
    /// connected <see cref="SmtpClient"/> instance for sending emails.
    /// </summary>
    /// <param name="builder">
    /// The <see cref="IHostApplicationBuilder" /> to read config from and add services to.
    /// </param>
    /// <param name="connectionName">
    /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
    /// </param>
    /// <param name="configureSettings">
    /// An optional delegate that can be used for customizing options.
    /// It's invoked after the settings are read from the configuration.
    /// </param>
    public static void AddMailKitClient(
        this IHostApplicationBuilder builder,
        string connectionName,
        Action<MailKitClientSettings>? configureSettings = null) =>
        AddMailKitClient(
            builder,
            MailKitClientSettings.DefaultConfigSectionName,
            configureSettings,
            connectionName,
            serviceKey: null);

    /// <summary>
    /// Registers 'Scoped' <see cref="MailKitClientFactory" /> for creating
    /// connected <see cref="SmtpClient"/> instance for sending emails.
    /// </summary>
    /// <param name="builder">
    /// The <see cref="IHostApplicationBuilder" /> to read config from and add services to.
    /// </param>
    /// <param name="name">
    /// The name of the component, which is used as the <see cref="ServiceDescriptor.ServiceKey"/> of the
    /// service and also to retrieve the connection string from the ConnectionStrings configuration section.
    /// </param>
    /// <param name="configureSettings">
    /// An optional method that can be used for customizing options. It's invoked after the settings are
    /// read from the configuration.
    /// </param>
    public static void AddKeyedMailKitClient(
        this IHostApplicationBuilder builder,
        string name,
        Action<MailKitClientSettings>? configureSettings = null)
    {
        ArgumentNullException.ThrowIfNull(name);

        AddMailKitClient(
            builder,
            $"{MailKitClientSettings.DefaultConfigSectionName}:{name}",
            configureSettings,
            connectionName: name,
            serviceKey: name);
    }

    private static void AddMailKitClient(
        this IHostApplicationBuilder builder,
        string configurationSectionName,
        Action<MailKitClientSettings>? configureSettings,
        string connectionName,
        object? serviceKey)
    {
        ArgumentNullException.ThrowIfNull(builder);

        var settings = new MailKitClientSettings();

        builder.Configuration
               .GetSection(configurationSectionName)
               .Bind(settings);

        if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
        {
            settings.ParseConnectionString(connectionString);
        }

        configureSettings?.Invoke(settings);

        if (serviceKey is null)
        {
            builder.Services.AddScoped(CreateMailKitClientFactory);
        }
        else
        {
            builder.Services.AddKeyedScoped(serviceKey, (sp, key) => CreateMailKitClientFactory(sp));
        }

        MailKitClientFactory CreateMailKitClientFactory(IServiceProvider _)
        {
            return new MailKitClientFactory(settings);
        }

        if (settings.DisableHealthChecks is false)
        {
            builder.Services.AddHealthChecks()
                .AddCheck<MailKitHealthCheck>(
                    name: serviceKey is null ? "MailKit" : $"MailKit_{connectionName}",
                    failureStatus: default,
                    tags: []);
        }

        if (settings.DisableTracing is false)
        {
            builder.Services.AddOpenTelemetry()
                .WithTracing(
                    traceBuilder => traceBuilder.AddSource(
                        Telemetry.SmtpClient.ActivitySourceName));
        }

        if (settings.DisableMetrics is false)
        {
            // Required by MailKit to enable metrics
            Telemetry.SmtpClient.Configure();

            builder.Services.AddOpenTelemetry()
                .WithMetrics(
                    metricsBuilder => metricsBuilder.AddMeter(
                        Telemetry.SmtpClient.MeterName));
        }
    }
}

上述程式代碼會在 IHostApplicationBuilder 類型上新增兩個擴充方法,一個用於 MailKit 的標準註冊,另一個用於 MailKit 的索引鍵註冊。

提示

.NET .NET Aspire 整合的擴充方法應該擴充 IHostApplicationBuilder 類型,並遵循 Add<MeaningfulName> 命名慣例,其中 <MeaningfulName> 是您新增的類型或功能。 在本文中,AddMailKitClient 擴充方法可用來新增MailKit client。 使用 AddMailKitSmtpClient 而不是 AddMailKitClient可能更符合官方指導方針,因為這樣做只會註冊 SmtpClient,而不是整個 MailKit 程式庫。

這兩個延伸模組最終都依賴私人 AddMailKitClient 方法,將 MailKitClientFactory 相依性插入容器註冊為 範圍服務。 將 MailKitClientFactory 註冊為範圍服務的原因是因為連線作業被視為開銷高昂,因此應該在可能的情況下,在相同範圍內重複使用。 換句話說,對於單一要求,應該使用相同的 ISmtpClient 實例。 工廠會保留它所建立的 SmtpClient 實例並加以處置。

組態系結

AddMailKitClient 方法私用實作的第一件事之一,就是將組態設定系結至 MailKitClientSettings 類別。 設定類別會被實例化,然後用組態中特定區段來呼叫 Bind。 然後使用目前的設定調用可選的 configureSettings 委派。 這可讓使用者進一步配置選項,確保手動程式代碼設定優先於組態設定。 接著,取決於是否提供 serviceKey 值,MailKitClientFactory 應該向相依注入容器註冊為標準或鍵值服務。

重要

在註冊服務時,會特意呼叫 "implementationFactory" 多載。 當組態無效時,CreateMailKitClientFactory 方法會擲回。 這可確保建立 MailKitClientFactory 會被延後,直到需要的時候,並防止應用程式在記錄可用之前出錯。

以下各節將更詳細地說明健康檢查與遙測的註冊。

新增健康檢查

健康檢查 是用來監控整合狀況的方法。 使用MailKit,您可以檢查 SMTP server 連線是否狀況良好。 在名為 MailKitHealthCheck.cs的檔案中,將下列程式代碼新增至 MailKit.Client 專案:

using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace MailKit.Client;

internal sealed class MailKitHealthCheck(MailKitClientFactory factory) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // The factory connects (and authenticates).
            _ = await factory.GetSmtpClientAsync(cancellationToken);

            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(exception: ex);
        }
    }
}

上述健康檢查執行:

  • 實作 IHealthCheck 介面。
  • 接受 MailKitClientFactory 做為主要建構函式參數。
  • 依照下列方式滿足 CheckHealthAsync 方法:
    • 嘗試從 factory取得 ISmtpClient 實例。 如果成功,則會傳回 HealthCheckResult.Healthy
    • 如果發生例外狀況,則會傳回 HealthCheckResult.Unhealthy

如同先前在註冊 MailKitClientFactory中共享的,MailKitHealthCheck 會有條件地向 IHeathChecksBuilder註冊:

if (settings.DisableHealthChecks is false)
{
    builder.Services.AddHealthChecks()
        .AddCheck<MailKitHealthCheck>(
            name: serviceKey is null ? "MailKit" : $"MailKit_{connectionName}",
            failureStatus: default,
            tags: []);
}

取用者可以選擇省略健康情況檢查,方法是將設定中的 DisableHealthChecks 屬性設定為 true。 整合的常見模式是具有選擇性的功能,.NET.NET Aspire 整合強烈建議這些類型的組態。 如需健康情況檢查和包含使用者介面的工作範例詳細資訊,請參閱 .NET AspireASP.NET Core HealthChecksUI 範例

連接遙測

作為最佳實踐,MailKit client 函式庫允許接觸到遙測功能。 .NET .NET Aspire 可以利用此遙測,然後將其顯示在 .NET.NET Aspire 儀錶板中。 根據是否啟用追蹤和計量,遙測將被設定如下列代碼段所示:

if (settings.DisableTracing is false)
{
    builder.Services.AddOpenTelemetry()
        .WithTracing(
            traceBuilder => traceBuilder.AddSource(
                Telemetry.SmtpClient.ActivitySourceName));
}

if (settings.DisableMetrics is false)
{
    // Required by MailKit to enable metrics
    Telemetry.SmtpClient.Configure();

    builder.Services.AddOpenTelemetry()
        .WithMetrics(
            metricsBuilder => metricsBuilder.AddMeter(
                Telemetry.SmtpClient.MeterName));
}

更新電子報服務

建立整合連結庫之後,您現在可以更新電子報服務,以使用MailKit client。 第一步是添加對 MailKit.Client 項目的引用。 新增 MailKit。Client.csprojMailDevResource.NewsletterService 項目的參考:

dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference MailKit.Client/MailKit.Client.csproj

接下來,新增 ServiceDefaults 項目的參考:

dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj

最後一個步驟是使用下列 C# 程式代碼取代 MailDevResource.NewsletterService 項目中現有的 Program.cs 檔案:

using System.Net.Mail;
using MailKit.Client;
using MailKit.Net.Smtp;
using MimeKit;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add services to the container.
builder.AddMailKitClient("maildev");

var app = builder.Build();

app.MapDefaultEndpoints();

// Configure the HTTP request pipeline.

app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();

app.MapPost("/subscribe",
    async (MailKitClientFactory factory, string email) =>
{
    ISmtpClient client = await factory.GetSmtpClientAsync();

    using var message = new MailMessage("newsletter@yourcompany.com", email)
    {
        Subject = "Welcome to our newsletter!",
        Body = "Thank you for subscribing to our newsletter!"
    };

    await client.SendAsync(MimeMessage.CreateFromMailMessage(message));
});

app.MapPost("/unsubscribe",
    async (MailKitClientFactory factory, string email) =>
{
    ISmtpClient client = await factory.GetSmtpClientAsync();

    using var message = new MailMessage("newsletter@yourcompany.com", email)
    {
        Subject = "You are unsubscribed from our newsletter!",
        Body = "Sorry to see you go. We hope you will come back soon!"
    };

    await client.SendAsync(MimeMessage.CreateFromMailMessage(message));
});

app.Run();

上述程式代碼中最值得注意的變更如下:

  • 已更新的 using 語句,其中包含 MailKit.ClientMailKit.Net.SmtpMimeKit 命名空間。
  • 使用呼叫 AddMailKitClient 擴充方法取代官方 .NETSmtpClient 的註冊。
  • /subscribe/unsubscribe 的映射呼叫替換為插入 MailKitClientFactory,並使用 ISmtpClient 實例傳送電子郵件。

執行範例

既然您已建立 MailKit client 整合並更新電子報服務以使用它,您可以執行範例。 從 IDE 中,選取 F5,或從解決方案的根目錄執行 dotnet run 以啟動應用程式,您應該會看到 .NET.NET Aspire 儀錶板

.NET Aspire 儀錶板:執行 MailDev 和電子報資源。

應用程式執行之後,流覽至 https://localhost:7251/swagger 的 Swagger UI,並測試 /subscribe/unsubscribe 端點。 選取向下箭號以展開端點:

Swagger UI:訂閱端點。

然後選取 [Try it out] 按鈕。 輸入電子郵件地址,然後選取 [Execute] 按鈕。

Swagger UI:使用電子郵件地址訂閱端點。

重複這個數次,以新增多個電子郵件位址。 您應該會看到傳送至 MailDev 收件匣的電子郵件:

多封電子郵件的 MailDev 收件匣。

選取執行應用程式的終端機視窗中 Ctrl+C,或選取 IDE 中的停止按鈕來停止應用程式。

檢視MailKit遙測

MailKit client 程式庫提供可以在 .NET Aspire 儀錶板中檢視的遙測資訊。 若要檢視遙測,請瀏覽至位於 https://localhost:7251的 .NET.NET Aspire 儀錶板。 選取 newsletter 資源以在 度量 頁面上檢視遙測資料:

.NET.NET Aspire 儀錶板:MailKit 遙測。

再次開啟 Swagger UI,並對 /subscribe/unsubscribe 端點提出一些要求。 然後,流覽回 .NET.NET Aspire 儀錶板,然後選取 newsletter 資源。 選取 mailkit.net.smtp 節點底下的指標,例如 mailkit.net.smtp.client.operation.count。 您應該會看到 MailKit 的 client遙測資料:

.NET.NET Aspire 儀錶板:MailKit 的作業數量遙測資料。

總結

在本文中,您已瞭解如何建立使用MailKit傳送電子郵件的 .NET.NET Aspire 整合。 您也瞭解如何將此整合整合到您先前建置的電子報應用程式中。 您已瞭解 .NET Aspire 整合的核心原則,例如透過相依性注入向取用者公開底層 client 程式庫,以及如何將健康情況檢查和遙測技術新增至整合。 您也已瞭解如何更新電子報服務,以使用 MailKit client。

去建造您自己的 .NET.NET Aspire 整合。 如果您認為您在建置的整合中有足夠的社群價值,請考慮將它發佈為 NuGet 套件, 供其他人使用。 此外,請考慮將提取要求提交至 .NET AspireGitHub 存放庫,以考慮包含在官方 .NET.NET Aspire 整合中。

後續步驟