Crie integrações de .NET Aspireclient personalizadas
Este artigo é uma continuação do artigo Criar integrações de hospedagem .NET.NET Aspire personalizadas. Ele orienta você através da criação de uma integração de .NET Aspireclient que usa MailKit para enviar e-mails. Essa integração é então adicionada ao aplicativo Newsletter que você criou anteriormente. O exemplo anterior omitiu a criação de uma integração client e, em vez disso, baseou-se no .NETSmtpClient
existente. É melhor usar o SmtpClient
do MailKit sobre o .NETSmtpClient
oficial para enviar e-mails, pois é mais moderno e suporta mais recursos / protocolos. Para obter mais informações, consulte .NET SmtpClient: Comments.
Pré-requisitos
Se estiveres a seguir, deverás ter uma aplicação de newsletter das etapas do artigo Criar integração de hospedagem .NET.NET Aspire personalizada.
Dica
Este artigo é inspirado nas integrações de .NET.NET Aspire existentes e baseado nas orientações oficiais da equipe. Há lugares onde essa orientação varia, e é importante entender o raciocínio por trás das diferenças. Para obter mais informações, consulte .NET.NET Aspire requisitos de integração.
Criar biblioteca para integração
.NET .NET Aspire integrações são entregues como pacotes NuGet, mas, neste exemplo, publicar um pacote NuGet está além do escopo do artigo. Em vez disso, você cria um projeto de biblioteca de classes que contém a integração e faz referência a ele como um projeto. Os pacotes de integração do .NET Aspire têm como objetivo envolver uma biblioteca client, como o MailKit, e fornecer telemetria pronta para produção, verificações de integridade, configurabilidade e testabilidade. Vamos começar criando um novo projeto de biblioteca de classes.
Crie um novo projeto de biblioteca de classes chamado
MailKit.Client
no mesmo diretório que o MailDevResource.sln do artigo anterior.dotnet new classlib -o MailKit.Client
Adicione o projeto à solução.
dotnet sln ./MailDevResource.sln add MailKit.Client/MailKit.Client.csproj
A próxima etapa é adicionar todos os pacotes NuGet nos quais a integração depende. Em vez de adicionar cada pacote um a um a partir da CLI .NET, provavelmente é mais fácil copiar e colar o XML seguinte no MailKit.Clientficheiro .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>
Definir configurações de integração
Sempre que estiveres a criar uma integração de .NET Aspire, é melhor compreender a biblioteca de client para a qual estás a mapear. Com o MailKit, você precisa entender as definições de configuração necessárias para se conectar a um protocolo SMTP (Simple Mail Transfer Protocol) server. Mas também é importante entender se a biblioteca tem suporte para verificações de integridade , rastreamento e métricas . O MailKit suporta de rastreamento e métricas de , através de sua classe Telemetry.SmtpClient
. Ao adicionar verificações de integridade, deve utilizar quaisquer verificações de integridade estabelecidas ou existentes sempre que possível. Caso contrário, podes considerar implementar o teu próprio na integração. Adicione o seguinte código ao projeto MailKit.Client
em um arquivo chamado MailKitClientSettings.cs:
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;
}
}
}
O código anterior define a classe MailKitClientSettings
com:
-
Endpoint
propriedade que representa a cadeia de conexão do SMTP server. -
DisableHealthChecks
propriedade que determina se as verificações de saúde estão habilitadas. -
DisableTracing
propriedade que determina se o rastreamento está habilitado. -
DisableMetrics
propriedade que determina se as métricas estão habilitadas.
Analisar lógica da cadeia de conexão
A classe settings também contém um método ParseConnectionString
que analisa a cadeia de conexão em um Uri
válido. Espera-se que a configuração seja fornecida no seguinte formato:
-
ConnectionStrings:<connectionName>
: A string de conexão para o SMTP server. -
MailKit:Client:ConnectionString
: A string de conexão para o SMTP server.
Se nenhum desses valores for fornecido, uma exceção será lançada.
Apresentar a funcionalidade client
O objetivo das integrações .NET Aspire é expor a biblioteca client subjacente aos consumidores por meio da injeção de dependência. Com o MailKit e para este exemplo, a classe SmtpClient
é o que você quer expor. Você não está encapsulando nenhuma funcionalidade, mas sim mapeando definições de configuração para uma classe SmtpClient
. É comum expor registros padrão e de serviço com chave para integrações. Os registros padrão são usados quando há apenas uma instância de um serviço, e os registros de serviço com chave são usados quando há várias instâncias de um serviço. Às vezes, para conseguir vários registros do mesmo tipo, você usa um padrão de fábrica. Adicione o seguinte código ao projeto MailKit.Client
em um arquivo chamado MailKitClientFactory.cs:
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();
}
}
A classe MailKitClientFactory
é uma fábrica que cria uma instância ISmtpClient
com base nas definições de configuração. Ele é responsável por retornar uma implementação de ISmtpClient
que tenha uma conexão ativa com um serverSMTP configurado. Em seguida, precisa-se expor a funcionalidade para que os consumidores registem essa fábrica no contêiner de injeção de dependência. Adicione o seguinte código ao projeto MailKit.Client
em um arquivo chamado MailKitExtensions.cs:
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));
}
}
}
O código anterior adiciona dois métodos de extensão no tipo IHostApplicationBuilder
, um para o registro padrão do MailKit e outro para o registro com chave do MailKit.
Dica
Os métodos de extensão para integrações .NET.NET Aspire devem estender o tipo de IHostApplicationBuilder
e seguir a convenção de nomenclatura Add<MeaningfulName>
, onde o <MeaningfulName>
é o tipo ou funcionalidade que você está adicionando. Para este artigo, o método de extensão AddMailKitClient
é usado para adicionar o MailKit client. É provável que esteja mais alinhado com a orientação oficial de usar AddMailKitSmtpClient
em vez de AddMailKitClient
, uma vez que isso registra apenas o SmtpClient
e não toda a biblioteca do MailKit.
Em última instância, ambas as extensões dependem do método AddMailKitClient
privado para registar o MailKitClientFactory
no contentor de injeção de dependências como um serviço de âmbito . A razão para registrar o MailKitClientFactory
como um serviço com escopo é porque as operações de conexão são consideradas caras e devem ser reutilizadas dentro do mesmo escopo sempre que possível. Por outras palavras, para um único pedido, deve ser utilizada a mesma instância ISmtpClient
. A fábrica mantém a instância do SmtpClient
que ela cria e depois descarta.
Vinculação de configuração
Uma das primeiras coisas que a implementação privada dos métodos AddMailKitClient
faz é vincular as definições de configuração à classe MailKitClientSettings
. A classe de configurações é instanciada e, em seguida, Bind
é invocada com a seção específica de configuração. Em seguida, o delegado configureSettings
opcional é invocado com as configurações atuais. Isso permite que o consumidor configure ainda mais detalhadamente as definições, garantindo que as configurações manuais de código sejam respeitadas sobre as configurações gerais. Depois disso, dependendo se o valor de serviceKey
foi fornecido, o MailKitClientFactory
deve ser registado no container de injeção de dependência como um serviço padrão ou identificado por chave.
Importante
É intencional que a sobrecarga de implementationFactory
seja chamada quando se registam os serviços. O método CreateMailKitClientFactory
é lançado quando a configuração é inválida. Isso garante que a criação do MailKitClientFactory
seja adiada até que seja necessária e evita que o aplicativo cometa erros antes que o log esteja disponível.
O registro de verificações de integridade e telemetria são descritos com um pouco mais de detalhes nas seções a seguir.
Adicionar verificações de integridade
Verificações de integridade são uma forma de monitorizar o estado de saúde de uma integração. Com o MailKit, pode verificar se a conexão ao SMTP server está boa. Adicione o seguinte código ao projeto MailKit.Client
em um arquivo chamado MailKitHealthCheck.cs:
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);
}
}
}
A implementação da verificação de integridade anterior:
- Implementa a interface
IHealthCheck
. - Aceita o
MailKitClientFactory
como um parâmetro primário do construtor. - Satisfaz o método
CheckHealthAsync
ao:- Tentando obter uma instância
ISmtpClient
dofactory
. Se for bem-sucedido, ele retornaráHealthCheckResult.Healthy
. - Se uma exceção for lançada, retorna
HealthCheckResult.Unhealthy
.
- Tentando obter uma instância
Como anteriormente compartilhado no registo do MailKitClientFactory
, o MailKitHealthCheck
é condicionalmente registado com o IHeathChecksBuilder
:
if (settings.DisableHealthChecks is false)
{
builder.Services.AddHealthChecks()
.AddCheck<MailKitHealthCheck>(
name: serviceKey is null ? "MailKit" : $"MailKit_{connectionName}",
failureStatus: default,
tags: []);
}
O consumidor pode optar por omitir verificações de integridade definindo a propriedade DisableHealthChecks
como true
na configuração. Um padrão comum para integrações é ter funcionalidades opcionais, e as integrações .NET.NET Aspire incentivam fortemente esses tipos de configurações. Para obter mais informações sobre verificações de integridade e um exemplo de trabalho que inclui uma interface do usuário, consulte .NET AspireASP.NET Core Exemplo de HealthChecksUI.
Configurar telemetria
Como melhor prática, a biblioteca MailKit client expõe a telemetria. .NET .NET Aspire pode aproveitar essa telemetria e exibi-la no painel .NET.NET Aspire. Dependendo se o rastreamento e as métricas estão habilitados ou não, a telemetria é conectada conforme mostrado no trecho de código a seguir:
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));
}
Atualizar o serviço de Newsletter
Com a biblioteca de integração criada, agora você pode atualizar o serviço de boletim informativo para usar o MailKit client. O primeiro passo é adicionar uma referência ao projeto MailKit.Client
. Adicionar a referência de projeto MailKit.Client.csproj ao projeto MailDevResource.NewsletterService
:
dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference MailKit.Client/MailKit.Client.csproj
Em seguida, adicione uma referência ao projeto ServiceDefaults
:
dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj
A etapa final é substituir o arquivo Program.cs existente no projeto MailDevResource.NewsletterService
com o seguinte código 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();
As alterações mais notáveis no código anterior são:
- As instruções
using
atualizadas que incluem os namespacesMailKit.Client
,MailKit.Net.Smtp
eMimeKit
. - A substituição do registo para o .NET
SmtpClient
oficial pela chamada para o método de extensãoAddMailKitClient
. - A substituição de
/subscribe
e/unsubscribe
mapear chamadas de postagem para, em vez disso, injetar oMailKitClientFactory
e usar a instânciaISmtpClient
para enviar o e-mail.
Executar o exemplo
Agora que você criou a integração do MailKit client e atualizou o serviço de boletim informativo para usá-lo, você pode executar o exemplo. No IDE, selecione F5 ou execute dotnet run
a partir do diretório raiz da solução para iniciar o aplicativo — você deve ver o painel .NET.NET Aspire:
Logo que a aplicação estiver a correr, navegue até à interface do utilizador do Swagger em https://localhost:7251/swagger e teste os endpoints /subscribe
e /unsubscribe
. Selecione a seta para baixo para expandir o ponto de extremidade:
Em seguida, selecione o botão Try it out
. Introduza um endereço de e-mail e, em seguida, selecione o botão Execute
.
Repita isso várias vezes para adicionar vários endereços de e-mail. Você deve ver o e-mail enviado para a caixa de entrada MailDev:
Pare o aplicativo selecionando Ctrl+C na janela do terminal onde o aplicativo está sendo executado, ou selecionando o botão Parar no IDE.
Ver a telemetria do MailKit
A biblioteca client do MailKit expõe telemetria visualizável no painel .NET Aspire. Para visualizar a telemetria, navegue até o painel .NET.NET Aspire em https://localhost:7251. Selecione o recurso
Abra o Swagger UI novamente e faça algumas solicitações aos pontos de extremidade /subscribe
e /unsubscribe
. Em seguida, navegue de volta para o painel .NET.NET Aspire e selecione o recurso newsletter
. Selecione uma métrica no nó mailkit.net.smtp, como mailkit.net.smtp.client.operation.count
. Você deve ver a telemetria para o MailKit client:
Resumo
Neste artigo, você aprendeu como criar uma integração de .NET.NET Aspire que usa o MailKit para enviar e-mails. Você também aprendeu como integrar essa integração no aplicativo Newsletter que você criou anteriormente. Você aprendeu sobre os princípios básicos das integrações de .NET Aspire, como expor a biblioteca subjacente client aos consumidores por meio da injeção de dependência e como adicionar verificações de saúde e telemetria à integração. Você também aprendeu como atualizar o serviço de boletim informativo para usar o MailKit client.
Avance e construa as suas próprias integrações .NET.NET Aspire. Se você acredita que há valor suficiente para a comunidade na integração que está criando, considere publicá-la como um pacote NuGet para outras pessoas usarem. Além disso, considere enviar um pull request para o repositório .NET AspireGitHub para consideração de inclusão nas integrações oficiais do .NET.NET Aspire.