Поделиться через


Создать пользовательские интеграции .NET Aspireclient

Эта статья является продолжением статьи Создание пользовательских .NET.NET Aspire интеграций хостинга. Он поможет вам создать интеграцию .NET Aspireclient, которая использует MailKit для отправки сообщений электронной почты. Затем эта интеграция добавляется в приложение бюллетеня, которое вы ранее создали. В предыдущем примере опущено создание интеграции client и вместо этого использовался существующий .NETSmtpClient. Лучше всего использовать SmtpClient MailKit по сравнению с официальным .NETSmtpClient для отправки сообщений электронной почты, так как это более современно и поддерживает больше функций и протоколов. Дополнительные сведения см. в статье .NET SmtpClient: примечания.

Необходимые условия

Если вы следуете инструкциям, у вас должно быть приложение для рассылки, следуя шагам, описанным в статье Create custom .NET.NET Aspire hosting integration.

Совет

Эта статья вдохновлена существующими интеграциями .NET.NET Aspire и основана на официальном руководстве команды. Есть места, где указанные рекомендации различаются, и важно понять причину различий. Дополнительные сведения см. в .NET.NET Aspire требованиях к интеграции.

Создание библиотеки для интеграции

.NET .NET Aspire интеграции предоставляются в виде пакетов NuGet, но в этом примере публикация пакета NuGet выходит за рамки обсуждаемой в статье темы. Вместо этого создается проект библиотеки классов, содержащий интеграцию и ссылающийся на него как проект. Пакеты интеграции .NET Aspire предназначены для упаковки библиотеки client, например MailKit, и предоставления готовой к использованию телеметрии, проверок работоспособности, возможности настройки и тестирования. Начнем с создания проекта библиотеки классов.

  1. Создайте проект библиотеки классов с именем MailKit.Client в том же каталоге, что и MailDevResource.sln из предыдущей статьи.

    dotnet new classlib -o MailKit.Client
    
  2. Добавьте проект в решение.

    dotnet sln ./MailDevResource.sln add MailKit.Client/MailKit.Client.csproj
    

Следующий шаг — добавить все пакеты NuGet, от которых зависит интеграция. Вместо того чтобы вы добавляли каждый пакет по одному в .NET CLI, вероятно, проще скопировать и вставить следующий XML-код в MailKit.Client.csproj-файл.

<ItemGroup>
  <PackageReference Include="MailKit" Version="4.9.0" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
  <PackageReference Include="Microsoft.Extensions.Resilience" Version="9.1.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" />
  <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.1" />
  <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
</ItemGroup>

Определение параметров интеграции

Каждый раз, когда вы создаете интеграцию .NET Aspire, в первую очередь важно понять библиотеку client, к которой вы сопоставляете. С помощью MailKit необходимо понять параметры конфигурации, необходимые для подключения к простому протоколу передачи почты (SMTP) server. Но также важно понимать, поддерживает ли библиотека поддержку проверок работоспособности, трассировки и метрик. MailKit поддерживает трассировки и метрикс помощью класса. При добавлении проверки работоспособностиследует по возможности использовать любые установленные или существующие проверки работоспособности. В противном случае можно рассмотреть возможность разработки собственного решения для интеграции. Добавьте следующий код в проект MailKit.Client в файл с именем 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;
        }
    }
}

Предыдущий код определяет класс MailKitClientSettings с:

  • Endpoint свойство, представляющее строку подключения к SMTP server.
  • DisableHealthChecks свойство, определяющее, включены ли проверки состояния.
  • DisableTracing свойство, определяющее, включена ли трассировка.
  • DisableMetrics свойство, определяющее, включены ли метрики.

Логика разбора строки подключения

Класс параметров также содержит метод ParseConnectionString, который анализирует строку подключения в валидный Uri. Ожидается, что конфигурация будет предоставлена в следующем формате:

  • ConnectionStrings:<connectionName>: строка подключения к SMTP-server.
  • MailKit:Client:ConnectionString: строка подключения к SMTP-server.

Если ни ни из этих значений не указано, создается исключение.

Раскрыть функциональность client

Целью интеграции .NET Aspire является предоставление базовой библиотеки client потребителям через внедрение зависимостей. С помощью MailKit и для этого примера класс SmtpClient является тем, что вы хотите предоставить. Вы не оборачиваете какую-либо функциональность, а скорее сопоставляете параметры конфигурации с классом SmtpClient. Обычно для интеграции можно предоставлять как стандартные, так и ключевые регистрации служб. Стандартные регистрации используются при наличии только одного экземпляра службы, а регистрация служб с ключами используется при наличии нескольких экземпляров службы. Иногда для получения нескольких регистраций одного типа используют шаблон фабрики. Добавьте следующий код в проект MailKit.Client в файл с именем 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();
    }
}

Класс MailKitClientFactory — это фабрика, которая создает экземпляр ISmtpClient на основе параметров конфигурации. Он обеспечивает возврат реализации ISmtpClient, которая имеет активное подключение к настроенному SMTP server. Затем необходимо предоставить функциональность для потребителей, чтобы зарегистрировать эту фабрику в контейнере внедрения зависимостей. Добавьте следующий код в проект MailKit.Client в файл с именем 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));
        }
    }
}

Предыдущий код добавляет два метода расширения для типа IHostApplicationBuilder, один для стандартной регистрации MailKit и другой для регистрации с ключами MailKit.

Совет

Методы расширения для интеграции .NET.NET Aspire должны расширить тип IHostApplicationBuilder и следовать соглашению об именовании Add<MeaningfulName>, где <MeaningfulName> является типом или функциональностью, которую вы добавляете. В этой статье метод расширения AddMailKitClient используется для добавления MailKit client. Скорее всего, использование AddMailKitSmtpClient вместо AddMailKitClientбудет больше соответствовать официальным рекомендациям, поскольку это регистрирует только SmtpClient, а не всю библиотеку MailKit.

В итоге оба расширения полагаются на частный метод AddMailKitClient, чтобы зарегистрировать MailKitClientFactory в контейнере внедрения зависимостей как службу с областью . Причина регистрации MailKitClientFactory в качестве ограниченной службы заключается в том, что операции подключения считаются дорогостоящими и следует повторно использовать в той же области, где это возможно. Другими словами, для одного запроса следует использовать тот же ISmtpClient экземпляр. Фабрика удерживает экземпляр SmtpClient, который она создает, и удаляет его.

Привязка конфигурации

Одной из первых вещей, которые выполняет частная реализация методов AddMailKitClient, является привязка параметров конфигурации к классу MailKitClientSettings. Класс параметров создаётся, и затем функция Bind вызывается с определённым разделом конфигурации. Затем вызывается необязательный configureSettings делегат с текущими параметрами. Это позволяет потребителю дополнительно настроить параметры, гарантируя, что параметры кода вручную имеют приоритет над параметрами конфигурации. После этого в зависимости от того, было ли предоставлено значение serviceKey, MailKitClientFactory следует зарегистрировать в контейнере внедрения зависимостей в качестве стандартной или ключевой службы.

Важный

Специально предусмотрено, что при регистрации служб вызывается перегрузка implementationFactory. Метод CreateMailKitClientFactory вызывается, если конфигурация недопустима. Это гарантирует, что создание MailKitClientFactory откладывается до тех пор, пока в нем не возникнет необходимость, и предотвращает сбой приложения до того, как ведение журнала станет доступным.

Регистрация проверок работоспособности, а также телеметрии, более подробно описана в следующих разделах.

Добавить проверки работоспособности

проверки состояния здоровья — это способ мониторинга состояния здоровья интеграций. С помощью MailKit можно проверить работоспособность подключения к SMTP-server. Добавьте следующий код в проект MailKit.Client в файл с именем 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);
        }
    }
}

Предыдущая реализация проверки состояния системы:

  • Реализует интерфейс IHealthCheck.
  • Принимает MailKitClientFactory в качестве основного параметра конструктора.
  • Удовлетворяет методу CheckHealthAsync следующими способами:
    • Попытка получить экземпляр ISmtpClient из factory. При успешном выполнении возвращается 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. Добавьте ссылку на проект MailKitClient.csproj в проект MailDevResource.NewsletterService.

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

Последний шаг — заменить существующий файл Program.cs в проекте MailDevResource.NewsletterService следующим кодом 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();

Наиболее заметными изменениями в предыдущем коде являются:

  • Обновленные инструкции using, включающие MailKit.Client, MailKit.Net.Smtpи пространства имен MimeKit.
  • Замена регистрации для официального .NETSmtpClient вызовом метода расширения AddMailKitClient.
  • Замена пост-вызовов для карт /subscribe и /unsubscribe на внедрение MailKitClientFactory и использование экземпляра ISmtpClient для отправки электронной почты.

Выполнить пример

Теперь, когда вы создали интеграцию MailKit client и обновили службу бюллетеня, чтобы использовать ее, можно запустить пример. В интегрированной среде разработки выберите F5 или запустите dotnet run из корневого каталога решения, чтобы запустить приложение — вы должны увидеть панель мониторинга .NET.NET Aspire.

панель мониторинга .NET Aspire: MailDev и ресурсы бюллетеня, работающие.

После запуска приложения перейдите к интерфейсу Swagger по адресу https://localhost:7251/swagger и протестируйте конечные точки /subscribe и /unsubscribe. Щелкните стрелку вниз, чтобы развернуть конечную точку:

в интерфейсе Swagger: подписка на конечную точку.

Затем нажмите кнопку Try it out. Введите адрес электронной почты и нажмите кнопку Execute.

Swagger UI: подписаться на конечную точку с адресом электронной почты.

Повторите это несколько раз, чтобы добавить несколько адресов электронной почты. Вы увидите сообщение электронной почты, отправленное в папку "Входящие" MailDev:

MailDev папка Входящие с несколькими электронными письмами.

Остановите приложение, выбрав CTRL+C в окне терминала, где запущено приложение, или нажав кнопку остановки в интегрированной среде разработки.

Просмотр телеметрии MailKit

Библиотека MailKit client предоставляет данные телеметрии, которые можно просмотреть на панели мониторинга .NET Aspire. Чтобы просмотреть данные телеметрии, перейдите на панель мониторинга .NET.NET Aspirehttps://localhost:7251. Выберите ресурс newsletter, чтобы просмотреть данные телеметрии на странице метрик .

панель мониторинга: телеметрия MailKit. .NET.NET Aspire

Снова откройте пользовательский интерфейс Swagger и выполните некоторые запросы к конечным точкам /subscribe и /unsubscribe. Затем вернитесь к панели мониторинга .NET.NET Aspire и выберите ресурс newsletter. Выберите метрику в узле mailkit.net.smtp, например mailkit.net.smtp.client.operation.count. Вы должны увидеть данные телеметрии для MailKit client:

панель мониторинга .NET.NET Aspire: телеметрия MailKit для подсчета операций.

Сводка

В этой статье вы узнали, как создать интеграцию .NET.NET Aspire, которая использует MailKit для отправки сообщений электронной почты. Вы также узнали, как интегрировать эту интеграцию с приложением бюллетеня, которое вы ранее создали. Вы узнали об основных принципах интеграции .NET Aspire, таких как предоставление базовой библиотеки client клиентам с помощью внедрения зависимостей и добавление проверок состояния и телеметрии в рамках интеграции. Вы также узнали, как обновить службу рассылки для использования MailKit client.

Идите вперед и создайте собственные интеграции .NET.NET Aspire. Если вы считаете, что в построенной интеграции достаточно ценности сообщества, рассмотрите возможность публикации его в качестве пакета NuGet, для других пользователей. Кроме того, рассмотрите возможность отправить пулл-реквест в репозиторий .NET AspireGitHub для рассмотрения и включения в официальные интеграции .NET.NET Aspire.

Дальнейшие действия