Compartilhar via


Criar integrações de .NET Aspireclient personalizadas

Este artigo é uma continuação do artigo Criar integrações personalizadas .NET.NET Aspire de hospedagem. Ele orienta você na criação de uma integração .NET Aspireclient que usa do MailKit para enviar emails. Essa integração é adicionada ao aplicativo Newsletter criado anteriormente. O exemplo anterior omitiu a criação de uma integração client e, em vez disso, se baseou no .NETSmtpClientexistente. É melhor usar o SmtpClient do MailKit no .NETSmtpClient oficial para enviar emails, pois ele é mais moderno e dá suporte a mais recursos/protocolos. Para obter mais informações, consulte .NET SmtpClient: Observações.

Pré-requisitos

Se você estiver acompanhando, deve ter um app de newsletter a partir das etapas descritas no artigo Criar integração personalizada de hospedagem .NET.NET Aspire.

Dica

Este artigo é inspirado em integrações .NET.NET Aspire existentes e baseado nas diretrizes 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, está além do escopo deste artigo publicar um pacote NuGet. 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 .NET Aspire destinam-se a encapsular 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.

  1. 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
    
  2. 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 da CLI do .NET, provavelmente é mais fácil copiar e colar o XML a seguir no MailKit.Clientarquivo .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 estiver criando uma integração .NET Aspire, é recomendável compreender bem a biblioteca client à qual você está mapeando. Com o MailKit, você precisa entender as configurações 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 dá suporte a de rastreamento e a métricas , por meio de sua classe Telemetry.SmtpClient. Ao adicionar verificações de integridade , você deve usar quaisquer verificações de integridade estabelecidas ou existentes sempre que possível. Caso contrário, você poderia considerar implementar a sua própria solução 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 ao SMTP server.
  • DisableHealthChecks propriedade que determina se as verificações de integridade estão habilitadas.
  • DisableTracing propriedade que determina se o rastreamento está habilitado.
  • DisableMetrics propriedade que determina se as métricas estão habilitadas.

Analisar a lógica da cadeia de conexão

A classe de configurações também contém um método ParseConnectionString que analisa a cadeia de conexão em um Uriválido. Espera-se que a configuração seja fornecida no seguinte formato:

  • ConnectionStrings:<connectionName>: string de conexão para o SMTP server.
  • MailKit:Client:ConnectionString: a cadeia de conexão para o serverSMTP.

Se nenhum desses valores for fornecido, uma exceção será gerada.

Exposição de funcionalidade de client

O objetivo das integrações de .NET Aspire é expor a biblioteca de 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ê deseja expor. Você não está encapsulando nenhuma funcionalidade, mas mapeando as configurações para uma classe SmtpClient. É comum expor registros padrão e de serviço 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 chave são usados quando há várias instâncias de um serviço. Às vezes, para obter 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 configurações. Ele é responsável por retornar uma implementação de ISmtpClient que tenha uma conexão ativa com um serverSMTP configurado. Em seguida, você precisa expor a funcionalidade para que os consumidores registrem essa fábrica com o 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 em chave do MailKit.

Ponta

Os métodos de extensão para as integrações .NET.NET Aspire devem estender o tipo IHostApplicationBuilder e seguir a convenção de nomenclatura Add<MeaningfulName>, onde <MeaningfulName> representa o tipo ou a funcionalidade que você está adicionando. Para este artigo, o método de extensão AddMailKitClient é usado para adicionar o componente clientdo MailKit. Provavelmente, é mais alinhado com as diretrizes oficiais para usar AddMailKitSmtpClient em vez de AddMailKitClient, já que isso só registra o SmtpClient e não toda a biblioteca do MailKit.

Ambas as extensões, em última análise, dependem do método privado AddMailKitClient para registrar o MailKitClientFactory no contêiner de injeção de dependência como um serviço com escopo . O motivo para registrar o MailKitClientFactory como um serviço com escopo é que as operações de conexão são consideradas caras e devem ser reutilizadas dentro do mesmo escopo, quando possível. Em outras palavras, para uma única solicitação, a mesma instância de ISmtpClient deve ser usada. A fábrica retém a instância da SmtpClient que cria e depois a descarta.

Vinculação de configuração

Uma das primeiras coisas que a implementação privada dos métodos AddMailKitClient faz é associar as configurações à classe MailKitClientSettings. A classe de configurações é instanciada e Bind é chamada com a seção específica da configuração. Em seguida, o delegado opcional configureSettings é invocado com as configurações atuais. Isso permite que o consumidor configure ainda mais as definições, garantindo que as configurações manuais de código tenham prioridade sobre as configurações automáticas. Depois disso, dependendo se o valor do serviceKey foi fornecido, o MailKitClientFactory deve ser registrado com o contêiner de injeção de dependência como um serviço padrão ou com chave.

Importante

É intencional que a sobrecarga de implementationFactory seja chamada ao registrar serviços. O método CreateMailKitClientFactory é gerado quando a configuração é inválida. Isso garante que a criação do MailKitClientFactory seja adiada até que seja necessária e impede que o aplicativo cometa erro antes que o processo de registro em log esteja disponível.

O registro de verificações de integridade e telemetria são descritos em um pouco mais de detalhes nas seções a seguir.

Adicionar verificações de integridade

Checagens de integridade são uma forma de monitorar a saúde de uma integração. Com o MailKit, você pode verificar se a conexão com o SMTP server está funcionando corretamente. 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 de verificação de integridade anterior:

  • Implementa a interface IHealthCheck.
  • Aceita o MailKitClientFactory como um parâmetro de construtor primário.
  • Satisfaz o método CheckHealthAsync por:
    • Tentando obter uma instância de ISmtpClient do factory. Se tiver êxito, ele retornará HealthCheckResult.Healthy.
    • Se uma exceção for gerada, retorna HealthCheckResult.Unhealthy.

Conforme compartilhado anteriormente no registro do MailKitClientFactory, o MailKitHealthCheck é registrado condicionalmente 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 para true na configuração. Um padrão comum para integrações é ter recursos opcionais e integrações .NET.NET Aspire incentiva 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 prática recomendada, a biblioteca clientMailKit expõede 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 seguinte snippet de código:

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 boletim informativo

Com a biblioteca de integração criada, agora você pode atualizar o serviço Boletim Informativo para usar o MailKit client. A primeira etapa é adicionar uma referência ao projeto de MailKit.Client. Adicione 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 de Program.cs existente no projeto MailDevResource.NewsletterService pelo 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 namespaces MailKit.Client, MailKit.Net.Smtpe MimeKit.
  • A substituição do registro oficial .NETSmtpClient pela chamada ao método de extensão AddMailKitClient.
  • A substituição das chamadas de postagem de /subscribe e /unsubscribe por injeções de MailKitClientFactory e o uso da instância de ISmtpClient para enviar o email.

Executar o exemplo

Agora que você criou a integração do MailKit client e atualizou o serviço Boletim Informativo para usá-lo, você pode executar o exemplo. No IDE, selecione F5 ou execute dotnet run no diretório raiz da solução para iniciar o aplicativo. Você deverá ver o painel .NET.NET Aspire:

.NET Aspire painel: MailDev e recursos do Boletim Informativo em execução.

Depois que o aplicativo estiver rodando, navegue até o Swagger UI em https://localhost:7251/swagger e teste os endpoints /subscribe e /unsubscribe. Selecione a seta para baixo para expandir o ponto de extremidade:

Swagger UI: endpoint de assinatura.

Em seguida, selecione o botão Try it out. Insira um endereço de email e selecione o botão Execute.

Swagger UI: inscrever-se no endpoint com o endereço de e-mail.

Repita isso várias vezes para adicionar vários endereços de email. Você deve ver o email enviado para a caixa de entrada MailDev:

MailDev caixa de entrada com vários emails.

Interrompa o aplicativo selecionando Ctrl+C na janela do terminal em que o aplicativo está em execução ou selecionando o botão parar em seu IDE.

Exibir telemetria do MailKit

A biblioteca de client do MailKit expõe a telemetria que pode ser exibida no painel .NET Aspire. Para exibir a telemetria, navegue até o painel .NET.NET Aspire em https://localhost:7251. Selecione o recurso para exibir a telemetria na página de Métricas do .

.NET.NET Aspire painel: telemetria do MailKit.

Abra a interface do Swagger novamente e faça algumas solicitações para os endpoints /subscribe e /unsubscribe. Em seguida, navegue de volta para o painel de .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ê deverá ver a telemetria do MailKit client:

.NET.NET Aspire painel de controle: telemetria do MailKit para contagem de operações.

Resumo

Neste artigo, você aprendeu a criar uma integração .NET.NET Aspire que usa o MailKit para enviar emails. Você também aprendeu a integrar essa integração ao aplicativo Newsletter criado anteriormente. Você aprendeu sobre os princípios fundamentais das integrações de .NET Aspire, como expor a biblioteca subjacente de client aos consumidores por meio da injeção de dependências e como adicionar verificações de saúde e telemetria à integração. Você também aprendeu a atualizar o serviço de newsletter para usar o MailKit client.

Avance e crie suas próprias integrações de .NET.NET Aspire. Se você acredita que há valor de comunidade suficiente na integração que você está criando, considere publicá-lo como um pacote NuGet para outras pessoas usarem. Além disso, considere enviar uma solicitação de pull para o repositório .NET AspireGitHub para consideração para inclusão nas integrações .NET.NET Aspire oficiais.

Próximas etapas