Partilhar via


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 .NETSmtpClientexistente. É 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.

  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 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 Urivá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 do factory. Se for bem-sucedido, ele retornará HealthCheckResult.Healthy.
    • Se uma exceção for lançada, retorna HealthCheckResult.Unhealthy.

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 namespaces MailKit.Client, MailKit.Net.Smtpe MimeKit.
  • A substituição do registo para o .NETSmtpClient oficial pela chamada para o método de extensão AddMailKitClient.
  • A substituição de /subscribe e /unsubscribe mapear chamadas de postagem para, em vez disso, injetar o MailKitClientFactory e usar a instância ISmtpClient 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:

.NET Aspire painel de controlo: recursos de MailDev e da Newsletter estão em execução.

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:

UI do Swagger: Ponto de subscrição.

Em seguida, selecione o botão Try it out. Introduza um endereço de e-mail e, em seguida, selecione o botão Execute.

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

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:

MailDev caixa de entrada com vários e-mails.

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 para exibir a telemetria na página Métricas :

.NET.NET Aspire painel: Telemetria MailKit.

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:

.NET.NET Aspire painel: Telemetria MailKit para contagem de operações.

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.

Próximos passos