次の方法で共有


カスタム .NET Aspireclient 統合を作成する

この記事は、「カスタム .NET.NET Aspire ホスティング統合の作成 続く記事です。 MailKit を使用して電子メールを送信する 統合 作成する手順について説明します。 この統合は、以前に作成したニュースレター アプリに追加されます。 前の例では、client 統合の作成を省略し、代わりに既存の .NETSmtpClientに依存していました。 メールを送信するための公式の .NETSmtpClient で MailKit の SmtpClient を使用することをお勧めします。これは、より最新であり、より多くの機能/プロトコルをサポートしているためです。 詳細については、「.NET SmtpClient: 解説」を参照してください。

前提 条件

フォローしている場合は、「カスタム .NET.NET Aspire ホスティング統合の作成」 記事の手順に従ってニュースレター アプリを作成する必要があります。

ヒント

この記事は、既存の .NET.NET Aspire 統合に基づき、チームの公式ガイダンスに基づいています。 ガイダンスが異なる場所があり、違いの背後にある理由を理解することが重要です。 詳細については、統合要件 .NET.NET Aspireを参照してください。

統合用のライブラリを作成する

.NET .NET Aspire 統合 は NuGet パッケージとして配信されますが、この例では、NuGet パッケージを発行するこの記事の範囲外です。 代わりに、統合を含むクラス ライブラリ プロジェクトを作成し、それをプロジェクトとして参照します。 .NET Aspire 統合パッケージは、MailKit などの client ライブラリをラップし、運用対応のテレメトリ、正常性チェック、構成可能性、およびテスト可能性を提供することを目的としています。 まず、新しいクラス ライブラリ プロジェクトを作成します。

  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 から各パッケージを 1 つずつ追加するのではなく、次の XML をコピーして MailKit に貼り付ける方が簡単です。.csproj ファイルをClientします。

<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 では、Telemetry.SmtpClient クラスを通じて、トレース および メトリックをサポートしています。 健康チェックを追加する場合は、可能な限り、既存または確立済みの健康チェックを使用する必要があります。 それ以外の場合は、統合で独自の実装を検討できます。 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 クラスには、接続文字列を解析して有効な Uriにする ParseConnectionString メソッドも含まれています。 構成は、次の形式で提供される必要があります。

  • ConnectionStrings:<connectionName>: SMTP serverへの接続文字列。
  • MailKit:Client:ConnectionString: SMTP serverへの接続文字列。

これらの値がどちらも指定されていない場合は、例外が発生します。

client 機能を公開する

.NET Aspire 統合の目的は、依存関係の挿入を通じて、基になる client ライブラリをコンシューマーに公開することです。 MailKit とこの例では、SmtpClient クラスを公開します。 機能をラップするのではなく、構成設定を SmtpClient クラスにマッピングします。 統合のために標準とキー付きサービスの両方の登録を公開するのが一般的です。 標準登録は、サービスのインスタンスが 1 つしかない場合に使用され、サービスのインスタンスが複数ある場合はキー付きサービスの登録が使用されます。 場合によっては、同じ型の複数の登録を実現するために、ファクトリ パターンを使用します。 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 インスタンスを作成するファクトリです。 構成された SMTP serverへのアクティブな接続を持つ ISmtpClient 実装を返す必要があります。 次に、コンシューマーがこのファクトリを依存関係挿入コンテナーに登録するための機能を公開する必要があります。 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 型に 2 つの拡張メソッドを追加します。1 つは MailKit の標準登録用で、もう 1 つは MailKit のキー付き登録用です。

チップ

.NET .NET Aspire 統合の拡張メソッドでは、IHostApplicationBuilder 型を拡張し、Add<MeaningfulName> 名前付け規則に従う必要があります。ここで、<MeaningfulName> は追加する型または機能です。 この記事では、AddMailKitClient 拡張メソッドを使用して MailKit clientを追加します。 AddMailKitClientではなく、AddMailKitSmtpClient を使用する公式のガイダンスに沿っている可能性が高くなります。これは、mailKit ライブラリ全体ではなく、SmtpClient のみを登録するためです。

どちらの拡張機能も、最終的にはプライベート AddMailKitClient メソッドに依存して、依存関係挿入コンテナーに MailKitClientFactoryスコープ サービスとして登録します。 MailKitClientFactory をスコープサービスとして登録する理由は、接続操作はコストが高いと見なされ、可能な場合は同じスコープ内で再利用する必要があるためです。 つまり、1 つの要求に対して、同じ ISmtpClient インスタンスを使用する必要があります。 ファクトリは、作成した SmtpClient のインスタンスを保持し、その後破棄します。

構成バインディング

AddMailKitClient メソッドのプライベート実装で最初に行うことの 1 つは、構成設定を 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 を追加します。MailDevResource.NewsletterService プロジェクトへの .csproj プロジェクト参照をClientします。

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

最後の手順では、MailDevResource.NewsletterService プロジェクト内の既存の Program.cs ファイルを次の C# コードに置き換えます。

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();

上記のコードで最も注目すべき変更点は次のとおりです。

  • MailKit.ClientMailKit.Net.Smtp、および MimeKit 名前空間を含む更新された using ステートメント。
  • 公式の .NETSmtpClient の登録を、AddMailKitClient 拡張メソッドの呼び出しによって置き換えます。
  • 代わりに MailKitClientFactory を挿入し、ISmtpClient インスタンスを使用して電子メールを送信するために、/subscribe/unsubscribe の両方のマップポスト呼び出しを置き換えます。

サンプルを実行する

MailKit client 統合を作成し、それを使用するようにニュースレター サービスを更新したので、サンプルを実行できます。 IDE から F5 選択するか、ソリューションのルート ディレクトリから を実行してアプリケーションを起動します。 ダッシュボードが表示されます。

.NET Aspire ダッシュボード: MailDev とニュースレターのリソースが実行されています。

アプリケーションが実行されたら、https://localhost:7251/swagger で Swagger UI に移動し、/subscribe/unsubscribe エンドポイントをテストします。 下矢印を選択してエンドポイントを展開します。

Swagger UI: エンドポイントをサブスクライブします。

次に、[Try it out] ボタンを選択します。 メール アドレスを入力し、[Execute] ボタンを選択します。

Swagger UI: 電子メール アドレスを使用してエンドポイントをサブスクライブします。

これを複数回繰り返して、複数のメール アドレスを追加します。 MailDev 受信トレイに送信された電子メールが表示されます。

複数のメールが入ったMailDevの受信トレイ。

アプリケーションが実行されているターミナル ウィンドウで CtrlC 選択するか、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.client.operation.countなど、mailkit.net.smtp ノードの下にあるメトリックを選択します。 MailKit clientのテレメトリが表示されます。

.NET.NET Aspire ダッシュボード: 操作件数の MailKit テレメトリー。

概要

この記事では、MailKit を使用して電子メールを送信する .NET.NET Aspire 統合を作成する方法について説明しました。 また、この統合を以前に作成したニュースレター アプリに統合する方法についても学習しました。 依存関係の挿入を通じて基になる client ライブラリをコンシューマーに公開する方法、統合に正常性チェックとテレメトリを追加する方法など、.NET Aspire 統合の基本原則について学習しました。 MailKit clientを使用するようにニュースレター サービスを更新する方法についても説明しました。

次に進み、独自の .NET.NET Aspire 統合を構築します。 構築している統合に十分なコミュニティ価値があると思われる場合は、他のユーザーが使用できる NuGet パッケージとして公開することを検討してください。 さらに、公式の .NET.NET Aspire 統合に含める検討のために、.NET AspireGitHub リポジトリ にプル要求を送信することを検討してください。

次の手順