Assegurar a comunicação entre hospedagem e integrações client
Este artigo é uma continuação de outros dois artigos anteriores que demonstram a criação de integrações personalizadas de hospedagem e de integrações personalizadas client.
Um dos principais benefícios de .NET.NET Aspire é como ele simplifica a facilidade de configuração de recursos e de clientes consumidores (ou integrações). Este artigo demonstra como compartilhar credenciais de autenticação de um recurso personalizado em uma integração de hospedagem para o consumidor client, em uma integração personalizada client. O recurso personalizado é um contêiner MailDev que permite credenciais de entrada ou saída. A integração personalizada client é uma client do MailKit que envia e-mails.
Pré-requisitos
Como este artigo continua do conteúdo anterior, você já deve ter criado a solução resultante como ponto de partida para este artigo. Caso ainda não tenha feito isso, conclua os seguintes artigos:
- Criar integrações personalizadas de hospedagem .NET.NET Aspire
- Criar integrações de .NET Aspireclient personalizadas
A solução resultante destes artigos anteriores contém os seguintes projetos:
- MailDev. Hospedagem: contém o tipo de recurso personalizado para o contêiner de MailDev.
- MailDevResource.AppHost: o host do aplicativo que usa o recurso personalizado e o define como uma dependência para um serviço de Boletim Informativo.
- MailDevResource.NewsletterService: um projeto de API Web ASP.NET Core que envia emails usando o contêiner MailDev.
- MailDevResource.ServiceDefaults: contém as configurações de serviço padrão destinadas ao compartilhamento.
-
MailKit.Client: contém a integração personalizada de client que expõe a
SmtpClient
do MailKit por meio de uma fábrica.
Atualizar o recurso MailDev
Para transferir credenciais de autenticação do recurso MailDev para a integração do MailKit, você precisa atualizar o recurso MailDev para incluir os parâmetros de nome de usuário e senha.
O contêiner MailDev dá suporte à autenticação básica para o protocolo de transferência de email simples de entrada e saída (SMTP). Para configurar as credenciais para entrada, você precisa definir as variáveis de ambiente MAILDEV_INCOMING_USER
e MAILDEV_INCOMING_PASS
. Para mais informações, veja MailDev: Uso de. Atualize o arquivo MailDevResource.cs no projeto MailDev.Hosting
, substituindo seu conteúdo pelo seguinte código C#:
// For ease of discovery, resource types should be placed in
// the Aspire.Hosting.ApplicationModel namespace. If there is
// likelihood of a conflict on the resource name consider using
// an alternative namespace.
namespace Aspire.Hosting.ApplicationModel;
public sealed class MailDevResource(
string name,
ParameterResource? username,
ParameterResource password)
: ContainerResource(name), IResourceWithConnectionString
{
// Constants used to refer to well known-endpoint names, this is specific
// for each resource type. MailDev exposes an SMTP and HTTP endpoints.
internal const string SmtpEndpointName = "smtp";
internal const string HttpEndpointName = "http";
private const string DefaultUsername = "mail-dev";
// An EndpointReference is a core .NET Aspire type used for keeping
// track of endpoint details in expressions. Simple literal values cannot
// be used because endpoints are not known until containers are launched.
private EndpointReference? _smtpReference;
/// <summary>
/// Gets the parameter that contains the MailDev SMTP server username.
/// </summary>
public ParameterResource? UsernameParameter { get; } = username;
internal ReferenceExpression UserNameReference =>
UsernameParameter is not null ?
ReferenceExpression.Create($"{UsernameParameter}") :
ReferenceExpression.Create($"{DefaultUsername}");
/// <summary>
/// Gets the parameter that contains the MailDev SMTP server password.
/// </summary>
public ParameterResource PasswordParameter { get; } = password;
public EndpointReference SmtpEndpoint =>
_smtpReference ??= new(this, SmtpEndpointName);
// Required property on IResourceWithConnectionString. Represents a connection
// string that applications can use to access the MailDev server. In this case
// the connection string is composed of the SmtpEndpoint endpoint reference.
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"Endpoint=smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)};Username={UserNameReference};Password={PasswordParameter}"
);
}
Essas atualizações adicionam uma propriedade UsernameParameter
e PasswordParameter
. Essas propriedades são usadas para armazenar os parâmetros para o MailDev nome de usuário e senha. A propriedade ConnectionStringExpression
é atualizada para incluir os parâmetros de nome de usuário e senha na cadeia de conexão. Em seguida, atualize o arquivo MailDevResourceBuilderExtensions.cs no projeto MailDev.Hosting
com o seguinte código C#:
using Aspire.Hosting.ApplicationModel;
// Put extensions in the Aspire.Hosting namespace to ease discovery as referencing
// the .NET Aspire hosting package automatically adds this namespace.
namespace Aspire.Hosting;
public static class MailDevResourceBuilderExtensions
{
private const string UserEnvVarName = "MAILDEV_INCOMING_USER";
private const string PasswordEnvVarName = "MAILDEV_INCOMING_PASS";
/// <summary>
/// Adds the <see cref="MailDevResource"/> to the given
/// <paramref name="builder"/> instance. Uses the "2.1.0" tag.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="httpPort">The HTTP port.</param>
/// <param name="smtpPort">The SMTP port.</param>
/// <returns>
/// An <see cref="IResourceBuilder{MailDevResource}"/> instance that
/// represents the added MailDev resource.
/// </returns>
public static IResourceBuilder<MailDevResource> AddMailDev(
this IDistributedApplicationBuilder builder,
string name,
int? httpPort = null,
int? smtpPort = null,
IResourceBuilder<ParameterResource>? userName = null,
IResourceBuilder<ParameterResource>? password = null)
{
var passwordParameter = password?.Resource ??
ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(
builder, $"{name}-password");
// The AddResource method is a core API within .NET Aspire and is
// used by resource developers to wrap a custom resource in an
// IResourceBuilder<T> instance. Extension methods to customize
// the resource (if any exist) target the builder interface.
var resource = new MailDevResource(
name, userName?.Resource, passwordParameter);
return builder.AddResource(resource)
.WithImage(MailDevContainerImageTags.Image)
.WithImageRegistry(MailDevContainerImageTags.Registry)
.WithImageTag(MailDevContainerImageTags.Tag)
.WithHttpEndpoint(
targetPort: 1080,
port: httpPort,
name: MailDevResource.HttpEndpointName)
.WithEndpoint(
targetPort: 1025,
port: smtpPort,
name: MailDevResource.SmtpEndpointName)
.WithEnvironment(context =>
{
context.EnvironmentVariables[UserEnvVarName] = resource.UserNameReference;
context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordParameter;
});
}
}
// This class just contains constant strings that can be updated periodically
// when new versions of the underlying container are released.
internal static class MailDevContainerImageTags
{
internal const string Registry = "docker.io";
internal const string Image = "maildev/maildev";
internal const string Tag = "2.1.0";
}
O código anterior atualiza o método de extensão AddMailDev
para incluir os parâmetros userName
e password
. O método WithEnvironment
é atualizado para incluir as variáveis de ambiente UserEnvVarName
e PasswordEnvVarName
. Essas variáveis de ambiente são usadas para definir o MailDev nome de usuário e senha.
Atualizar o host do aplicativo
Agora que o recurso é atualizado para incluir os parâmetros de nome de usuário e senha, você precisa atualizar o host do aplicativo para incluir esses parâmetros. Atualize o arquivo Program.cs no projeto MailDevResource.AppHost
com o seguinte código C#:
var builder = DistributedApplication.CreateBuilder(args);
var mailDevUsername = builder.AddParameter("maildev-username");
var mailDevPassword = builder.AddParameter("maildev-password");
var maildev = builder.AddMailDev(
name: "maildev",
userName: mailDevUsername,
password: mailDevPassword);
builder.AddProject<Projects.MailDevResource_NewsletterService>("newsletterservice")
.WithReference(maildev);
builder.Build().Run();
O código anterior adiciona dois parâmetros para o MailDev nome de usuário e senha. Ele atribui esses parâmetros às variáveis de ambiente MAILDEV_INCOMING_USER
e MAILDEV_INCOMING_PASS
. O método AddMailDev
tem duas chamadas encadeadas para WithEnvironment
que inclui essas variáveis de ambiente. Para obter mais informações sobre parâmetros, consulte Parâmetros externos.
Em seguida, configure os segredos para esses parâmetros. Clique com o botão direito do mouse no projeto MailDevResource.AppHost
e selecione Manage User Secrets
. Adicione o JSON a seguir aos segredos do .json arquivo:
{
"Parameters:maildev-username": "@admin",
"Parameters:maildev-password": "t3st1ng"
}
Aviso
Essas credenciais são apenas para fins de demonstração e MailDev destina-se ao desenvolvimento local. Essas credenciais são fictícias e não devem ser usadas em um ambiente de produção.
Atualizar a integração do MailKit
É uma boa prática que integrações client esperem que as cadeias de conexão contenham vários pares chave/valor e interpretem esses pares nas propriedades apropriadas. Atualize o arquivo MailKitClientSettings.cs no projeto MailKit.Client
com o seguinte código C#:
using System.Data.Common;
using System.Net;
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 the network credentials that are optionally configurable for SMTP
/// server's that require authentication.
/// </summary>
/// <value>
/// The default value is <see langword="null"/>.
/// </value>
public NetworkCredential? Credentials { 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;
if (builder.TryGetValue("Username", out var username) &&
builder.TryGetValue("Password", out var password))
{
Credentials = new(
username.ToString(), password.ToString());
}
}
}
}
A classe de configurações anterior agora inclui uma propriedade Credentials
do tipo NetworkCredential
. O método ParseConnectionString
é atualizado para analisar as chaves Username
e Password
da cadeia de conexão. Se as chaves Username
e Password
estiverem presentes, um NetworkCredential
será criado e atribuído à propriedade Credentials
.
Com a classe de configurações atualizada para entender e preencher as credenciais, atualize a classe de fábrica para utilizar as credenciais de forma condicional, se elas estiverem configuradas. Atualize o arquivo MailKitClientFactory.cs no projeto MailKit.Client
com o seguinte código C#:
using System.Net;
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);
if (settings.Credentials is not null)
{
await _client.AuthenticateAsync(settings.Credentials, cancellationToken)
.ConfigureAwait(false);
}
}
}
finally
{
_semaphore.Release();
}
return _client;
}
public void Dispose()
{
_client?.Dispose();
_semaphore.Dispose();
}
}
Quando a fábrica determina que as credenciais foram configuradas, ela se autentica com o SMTP server, após se conectar, antes de retornar o SmtpClient
.
Executar o exemplo
Agora que você atualizou o recurso, os projetos de integração correspondentes e o host do aplicativo, você está pronto para executar o aplicativo de exemplo. Para executar o exemplo do IDE, selecione F5 ou use dotnet run
no diretório raiz da solução para iniciar o aplicativo. Você deve ver o painel .NET.NET Aspire. Navegue até o recurso de contêiner maildev
e exiba os detalhes. Você deverá ver os parâmetros de nome de usuário e senha nos detalhes do recurso, na seção variáveis de ambiente:
Da mesma forma, você deverá ver a string de conexão nos detalhes do recurso newsletterservice
, na seção de variáveis de ambiente :
Valide se tudo está funcionando conforme o esperado.
Resumo
Este artigo demonstrou como transferir credenciais de autenticação de um recurso personalizado para uma integração de client personalizada. O recurso personalizado é um contêiner MailDev que permite credenciais de entrada ou saída. A integração personalizada de client é um client do MailKit que envia emails. Ao atualizar o recurso para incluir os parâmetros username
e password
e atualizar a integração para interpretar e usar esses parâmetros, as credenciais são transferidas pelo fluxo de autenticação da integração de hospedagem para a integração client.