共用方式為


建立自訂 Aspire 用戶端整合

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

先決條件

如果您按照步驟進行操作,您應該從建立自訂Aspire 代管整合一文中獲得一個電子報應用程式。

提示

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

建立整合函式庫

Aspire 整合 會以 NuGet 套件的形式傳遞,但在此範例中,發佈 NuGet 套件超出了本文的範圍。 相反地,您會建立包含整合的類別庫專案,並將其參考為專案。 Aspire 整合套件旨在包裝用戶端程式庫 (例如 MailKit),並提供生產就緒的遙測、健康情況檢查、可設定性和可測試性。 讓我們從建立新的類別庫項目開始。

  1. 在與上一篇文章中 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.14.1" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
  <PackageReference Include="Microsoft.Extensions.Resilience" Version="10.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
  <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.0" />
  <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
</ItemGroup>

定義整合設定

在建立 Aspire 整合時,最理想的是先瞭解您要對應的用戶端程式庫的功能和特性。 使用MailKit時,您必須瞭解連線到簡單郵件傳輸通訊協定 (SMTP) 伺服器所需的組態設定。 但還需了解程式庫是否支援 健康情況檢查追蹤計量。 MailKit 透過其 類別支援 追蹤指標。 新增 健康檢查時,您應盡可能使用任何已建立或現有的健康檢查。 否則,您可能會考慮在整合過程中實作自己的方案。 在名為 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 伺服器的連接字串。
  • DisableHealthChecks 屬性,用以啟用或停用健康檢查。
  • DisableTracing 屬性,決定是否啟用追蹤。
  • DisableMetrics 屬性,決定是否啟用計量。

剖析連接字串邏輯

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

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

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

公開用戶端功能

整合的目標 Aspire 是透過相依性插入將基礎用戶端程式庫公開給取用者。 使用MailKit和此範例時,SmtpClient 類別就是您想要公開的內容。 您不會包裝任何功能,而是將組態設定對應至 SmtpClient 類別。 對於整合來說,公開標準及鍵控服務的註冊是很常見的。 當服務只有一個實例時,就會使用標準註冊,而當有多個服務實例時,會使用索引鍵服務註冊。 有時候,若要建立相同類型的多個註冊,可以使用工廠模式。 在名為 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
{

    /// <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)
    {
        var client = new SmtpClient();
        try
        {
            if (settings.Endpoint is not null)
            {
                await client.ConnectAsync(settings.Endpoint, cancellationToken)
                             .ConfigureAwait(false);
            }
            return client;
        }
        catch
        {
            await client.DisconnectAsync(true, cancellationToken)
            client.Dispose();
            throw;
        }       
    }
}

MailKitClientFactory 類別是一個處理站,會根據組態設定建立 ISmtpClient 實例。 它負責返回一個具有有效連接到已配置的 SMTP 伺服器的 ISmtpClient 實現。 接下來,您需要公開功能以便讓使用者能在相依注入容器中註冊此工廠。 在名為 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 的索引鍵註冊。

提示

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

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

組態系結

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

重要

在註冊服務時,特意呼叫 implementationFactory 多載。 當組態無效時,CreateMailKitClientFactory 方法會擲回。 這可確保只在需要時再延後建立 MailKitClientFactory,同時並防止應用程式在記錄功能可用之前便發生錯誤。

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

新增健康檢查

健康檢查 是用來監控整合狀況的方法。 使用MailKit,您可以檢查 SMTP 伺服器的連線是否狀況良好。 在名為 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 方法:
    • 嘗試從 ISmtpClient取得 factory 實例。 如果成功,則會傳回 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。 整合的常見模式是具有選用功能,而 Aspire 整合強烈鼓勵這些類型的設定。 如需健康情況檢查和包含使用者介面的工作範例詳細資訊,請參閱 AspireASP.NET Core HealthChecksUI 範例

啟用遙測功能

最佳做法是,MailKit 用戶端程式庫會提供遙測功能。 Aspire可以利用此遙測並將其顯示在儀表板中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用戶端。 第一步是添加對 MailKit.Client 項目的引用。 將 MailKit.Client.csproj 專案引用新增到 MailDevResource.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# 程式代碼取代 Program.cs 項目中現有的 MailDevResource.NewsletterService 檔案:

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 命名空間。
  • 使用對.NET擴充方法的呼叫來取代SmtpClientAddMailKitClient的官方註冊。
  • /subscribe/unsubscribe 的映射呼叫替換為插入 MailKitClientFactory,並使用 ISmtpClient 實例傳送電子郵件。

執行範例

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

.NET AspireMailDev and Newsletter resources running.

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

Swagger UI:訂閱端點。

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

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

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

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

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

檢視MailKit遙測

MailKit 用戶端程式庫會公開可以在儀表板中查看的遙測資訊。 若要檢視遙測,請前往 Aspire 儀表板 https://localhost:7251。 選取 newsletter 資源以在 度量 頁面上檢視遙測資料:

Aspire 儀表板:MailKit 遙測。

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

Aspire 儀表板:MailKit 遙測以計算操作數量。

總結

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

繼續前進,構建您自己的 Aspire 整合。 如果您認為您在建置的整合中有足夠的社群價值,請考慮將它發佈為 NuGet 套件, 供其他人使用。 此外,請考慮向 AspireGitHub 儲存庫 提交提取請求,以考慮將其包含在官方 Aspire 整合中。

後續步驟