カスタム .NET Aspireclient 統合を作成する
この記事は、「カスタム .NET.NET Aspire ホスティング統合の作成 続く記事です。 MailKit を使用して電子メールを送信する SmtpClient
に依存していました。 メールを送信するための公式の .NETSmtpClient
で MailKit の SmtpClient
を使用することをお勧めします。これは、より最新であり、より多くの機能/プロトコルをサポートしているためです。 詳細については、「.NET SmtpClient: 解説」を参照してください。
前提 条件
フォローしている場合は、「カスタム .NET.NET Aspire ホスティング統合の作成」 記事の手順に従ってニュースレター アプリを作成する必要があります。
ヒント
この記事は、既存の .NET.NET Aspire 統合に基づき、チームの公式ガイダンスに基づいています。 ガイダンスが異なる場所があり、違いの背後にある理由を理解することが重要です。 詳細については、統合要件 .NET.NET Aspireを参照してください。
統合用のライブラリを作成する
.NET .NET Aspire 統合 は NuGet パッケージとして配信されますが、この例では、NuGet パッケージを発行するこの記事の範囲外です。 代わりに、統合を含むクラス ライブラリ プロジェクトを作成し、それをプロジェクトとして参照します。 .NET Aspire 統合パッケージは、MailKit などの client ライブラリをラップし、運用対応のテレメトリ、正常性チェック、構成可能性、およびテスト可能性を提供することを目的としています。 まず、新しいクラス ライブラリ プロジェクトを作成します。
前の記事の MailDevResource.sln と同じディレクトリに、
MailKit.Client
という名前の新しいクラス ライブラリ プロジェクトを作成します。dotnet new classlib -o MailKit.Client
ソリューションにプロジェクトを追加します。
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.Client
、MailKit.Net.Smtp
、およびMimeKit
名前空間を含む更新されたusing
ステートメント。 - 公式の .NET
SmtpClient
の登録を、AddMailKitClient
拡張メソッドの呼び出しによって置き換えます。 - 代わりに
MailKitClientFactory
を挿入し、ISmtpClient
インスタンスを使用して電子メールを送信するために、/subscribe
と/unsubscribe
の両方のマップポスト呼び出しを置き換えます。
サンプルを実行する
MailKit client 統合を作成し、それを使用するようにニュースレター サービスを更新したので、サンプルを実行できます。 IDE から F5
アプリケーションが実行されたら、https://localhost:7251/swagger で Swagger UI に移動し、/subscribe
と /unsubscribe
エンドポイントをテストします。 下矢印を選択してエンドポイントを展開します。
次に、[Try it out
] ボタンを選択します。 メール アドレスを入力し、[Execute
] ボタンを選択します。
これを複数回繰り返して、複数のメール アドレスを追加します。 MailDev 受信トレイに送信された電子メールが表示されます。
複数のメールが入った
アプリケーションが実行されているターミナル ウィンドウで Ctrl
MailKit テレメトリを表示する
MailKit client ライブラリは、.NET Aspire ダッシュボードで表示できるテレメトリを公開します。 テレメトリを表示するには、https://localhost:7251にある .NET.NET Aspire ダッシュボードに移動します。
newsletter
リソースを選択して、メトリック ページでテレメトリを表示します。
Swagger UI をもう一度開き、/subscribe
と /unsubscribe
エンドポイントにいくつかの要求を行います。 次に、.NET.NET Aspire ダッシュボードに戻り、newsletter
リソースを選択します。
mailkit.net.smtp.client.operation.count
など、mailkit.net.smtp ノードの下にあるメトリックを選択します。 MailKit clientのテレメトリが表示されます。
概要
この記事では、MailKit を使用して電子メールを送信する .NET.NET Aspire 統合を作成する方法について説明しました。 また、この統合を以前に作成したニュースレター アプリに統合する方法についても学習しました。 依存関係の挿入を通じて基になる client ライブラリをコンシューマーに公開する方法、統合に正常性チェックとテレメトリを追加する方法など、.NET Aspire 統合の基本原則について学習しました。 MailKit clientを使用するようにニュースレター サービスを更新する方法についても説明しました。
次に進み、独自の .NET.NET Aspire 統合を構築します。 構築している統合に十分なコミュニティ価値があると思われる場合は、他のユーザーが使用できる
次の手順
.NET Aspire