创建自定义 .NET.NET Aspire 托管集成
.NET .NET Aspire 通过提供可重用的构建模块来提高开发体验,这些模块可用于快速管理应用程序依赖并使其可用于您自己的代码。 基于 Aspire的应用程序的关键构建基块之一是 资源。 请考虑以下代码:
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var db = builder.AddPostgres("pgserver")
.AddDatabase("inventorydb");
builder.AddProject<Projects.InventoryService>("inventoryservice")
.WithReference(redis)
.WithReference(db);
在前面的代码中,有四个资源表示:
-
cache
:Redis 容器。 -
pgserver
:Postgres 容器。 -
inventorydb
:托管在pgserver
上的数据库。 -
inventoryservice
:ASP.NET Core 应用程序。
平均开发人员编写的大多数 .NET.NET Aspire相关代码都集中在将资源添加到 应用模型 并在它们之间创建引用。
.NET .NET Aspire 自定义资源的关键元素
在 .NET.NET Aspire 中生成自定义资源需要以下各项:
- 实现 IResource 的自定义资源类型
- 一个为 IDistributedApplicationBuilder 命名为
Add{CustomResource}
的扩展方法,其中{CustomResource}
是自定义资源的名称。
当自定义资源需要可选配置时,开发人员可能希望实现 With*
后缀扩展方法,以便于通过 生成器模式轻松发现这些配置选项。
一个实际示例:MailDev
为了帮助了解如何开发自定义资源,本文演示了如何为 MailDev生成自定义资源的示例。 MailDev 是一种开源工具,它提供了一个本地邮件 server,旨在允许开发人员在其应用中测试电子邮件发送行为。 有关详细信息,请参阅 MailDevGitHub 存储库。
在此示例中,将创建一个新的 .NET Aspire 项目作为所创建的 MailDev 资源的测试环境。 虽然可以在现有 .NET Aspire 项目中创建自定义资源,但最好考虑自定义资源是否可以跨多个基于 .NET Aspire的解决方案使用,并且应作为可重用集成进行开发。
设置起始项目
创建一个新的 .NET.NET Aspire 项目,用于测试我们正在开发的新资源。
dotnet new aspire -o MailDevResource
cd MailDevResource
dir
创建项目后,应会看到包含以下项的列表:
-
MailDevResource.AppHost
:用于测试自定义资源的 应用主机。 -
MailDevResource.ServiceDefaults
:服务默认 项目用于与服务相关的项目。 -
MailDevResource.sln
:引用这两个项目的解决方案文件。
通过执行以下命令验证项目是否可以成功生成并运行:
dotnet run --project MailDevResource.AppHost/MailDevResource.AppHost.csproj
控制台输出应如下所示:
Building...
info: Aspire.Hosting.DistributedApplication[0]
Aspire version: 9.0.0
info: Aspire.Hosting.DistributedApplication[0]
Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
Application host directory is:
..\docs-aspire\docs\extensibility\snippets\MailDevResource\MailDevResource.AppHost
info: Aspire.Hosting.DistributedApplication[0]
Now listening on: https://localhost:17251
info: Aspire.Hosting.DistributedApplication[0]
Login to the dashboard at https://localhost:17251/login?t=928db244c720c5022a7a9bf5cf3a3526
info: Aspire.Hosting.DistributedApplication[0]
Distributed application started. Press Ctrl+C to shut down.
在浏览器中选择 仪表板链接 以查看 .NET.NET Aspire 仪表板:
按 Ctrl+C 关闭应用(可以关闭浏览器选项卡)。
创建资源扩展库
.NET Aspire 资源只是引用 .NET Aspire 托管库(Aspire.Hosting
)的类库中包含的类和方法。 通过将资源放置在单独的项目中,可以更轻松地在基于 .NET.NET Aspire的应用之间共享资源,并在 NuGet 上打包和共享它。
创建名为 MailDev的类库项目。承载。
dotnet new classlib -o MailDev.Hosting
将
Aspire.Hosting
作为包引用添加到类库。dotnet add ./MailDev.Hosting/MailDev.Hosting.csproj package Aspire.Hosting --version 9.0.0
重要
此处指定的版本应与安装的 .NET.NET Aspire 工作负荷的版本匹配。
将类库引用添加到 MailDevResource.AppHost 项目。
dotnet add ./MailDevResource.AppHost/MailDevResource.AppHost.csproj reference ./MailDev.Hosting/MailDev.Hosting.csproj
将类库项目添加到解决方案文件。
dotnet sln ./MailDevResource.sln add ./MailDev.Hosting/MailDev.Hosting.csproj
执行以下步骤后,可以启动项目:
dotnet run --project ./MailDevResource.AppHost/MailDevResource.AppHost.csproj
这会导致向控制台显示警告:
.\.nuget\packages\aspire.hosting.apphost\9.0.0\build\Aspire.Hosting.AppHost.targets(174,5): warning ASPIRE004: '..\MailDev.Hosting\MailDev.Hosting.csproj' is referenced by an A
spire Host project, but it is not an executable. Did you mean to set IsAspireProjectResource="false"? [D:\source\repos\docs-aspire\docs\extensibility\snippets\MailDevResource\MailDevResource.AppHost\MailDevRe
source.AppHost.csproj]
这是因为 .NET.NET Aspire 将应用主机中的项目引用视为服务项目。 若要告知 .NET.NET Aspire 应将项目引用视为非服务项目,请修改对 MailDev.Hosting
项目的 MailDevResource.AppHostMailDevResource.AppHost.csproj 文件引用,如下所示:
<ItemGroup>
<!-- The IsAspireProjectResource attribute tells .NET Aspire to treat this
reference as a standard project reference and not attempt to generate
a metadata file -->
<ProjectReference Include="..\MailDev.Hosting\MailDev.Hosting.csproj"
IsAspireProjectResource="false" />
</ItemGroup>
现在,启动应用主机时,控制台上没有显示任何警告。
定义资源类型
MailDev。托管 类库包含用于将资源添加到应用主机的资源类型和扩展方法。 应首先考虑在使用自定义资源时要为开发人员提供的体验。 对于此自定义资源,你希望开发人员能够编写如下所示的代码:
var builder = DistributedApplication.CreateBuilder(args);
var maildev = builder.AddMailDev("maildev");
builder.AddProject<Projects.NewsletterService>("newsletterservice")
.WithReference(maildev);
为此,需要一个名为 MailDevResource
的自定义资源来实现 IResourceWithConnectionString,以便使用者可以将它与 WithReference 扩展一起使用,以将 MailDevserver 的连接详细信息作为连接字符串注入。
MailDev 可以作为容器资源使用,因此你还需要从 ContainerResource 派生,这样我们才能在 .NET.NET Aspire中使用各种已有的以容器为中心的扩展。
将 MailDev.Hosting
项目中 Class1.cs 文件的内容替换为以下代码,并将该文件重命名为 MailDevResource.cs:
// 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) : ContainerResource(name), IResourceWithConnectionString
{
// Constants used to refer to well known-endpoint names, this is specific
// for each resource type. MailDev exposes an SMTP endpoint and a HTTP
// endpoint.
internal const string SmtpEndpointName = "smtp";
internal const string HttpEndpointName = "http";
// 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;
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(
$"smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)}"
);
}
在前面的自定义资源中,EndpointReference 和 ReferenceExpression 是多种类型的示例,这些类型可实现接口集合,例如 IManifestExpressionProvider、IValueProvider和 IValueWithReferences。 有关这些类型及其 .NET.NET Aspire角色的详细信息,请参阅 技术详细信息。
定义资源扩展
为了便于开发人员使用自定义资源,需要将名为 AddMailDev
的扩展方法添加到 MailDev.Hosting 项目中。
AddMailDev
扩展方法负责配置资源,以便它可以作为容器成功启动。
将以下代码添加到 MailDev.Hosting 项目中一个新文件,文件名为 MailDevResourceBuilderExtensions.cs。
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
{
/// <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)
{
// 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);
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);
}
}
// 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";
}
验证应用主机内的自定义集成
现在,自定义资源的基本结构已完成,现在可以在实际的 AppHost 项目中对其进行测试了。 在 MailDevResource.AppHost 项目中打开 Program.cs 文件,并使用以下代码对其进行更新:
var builder = DistributedApplication.CreateBuilder(args);
var maildev = builder.AddMailDev("maildev");
builder.Build().Run();
更新 Program.cs 文件后,启动应用主机项目并打开仪表板:
dotnet run --project ./MailDevResource.AppHost/MailDevResource.AppHost.csproj
片刻后,仪表板会显示 maildev
资源正在运行,并且将有一个超链接导航到 MailDev Web 应用,其中显示了应用发送的每个电子邮件的内容。
.NET .NET Aspire 仪表板应如下所示:
MailDev Web 应用程序应如下所示:
将 .NET 服务项目添加到应用主机以进行测试
.NET Aspire 成功完成 MailDev 集成后,即可在 .NET 项目中使用 MailDev 的连接信息。 在 .NET.NET Aspire,通常会有一个 托管包 和一个或多个 组件包。 例如,请考虑:
-
托管包:用于表示应用模型中的资源。
Aspire.Hosting.Redis
-
组件包:用于配置和使用 client 库。
Aspire.StackExchange.Redis
Aspire.StackExchange.Redis.DistributedCaching
Aspire.StackExchange.Redis.OutputCaching
对于 MailDev 资源,.NET 平台已经有一个简单的邮件传输协议(SMTP)client,其形式为 SmtpClient。 在此示例中,为了简单起见,使用此现有 API,尽管其他资源类型可能受益于自定义集成库来帮助开发人员。
为了测试端到端方案,需要一个 .NET 项目,我们可以将连接信息注入到 MailDev 资源中。 添加 Web API 项目:
创建名为 MailDevResource.NewsletterService的新 .NET 项目。
dotnet new webapi --use-minimal-apis -o MailDevResource.NewsletterService
添加对 MailDev.Hosting 项目的引用。
dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference ./MailDev.Hosting/MailDev.Hosting.csproj
添加对 MailDevResource.AppHost 项目的引用。
dotnet add ./MailDevResource.AppHost/MailDevResource.AppHost.csproj reference ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj
将新项目添加到解决方案文件。
dotnet sln ./MailDevResource.sln add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj
添加项目并更新引用后,打开 MailDevResource.AppHost.csproj 项目的 Program.cs,并更新源文件,使其如下所示:
var builder = DistributedApplication.CreateBuilder(args);
var maildev = builder.AddMailDev("maildev");
builder.AddProject<Projects.MailDevResource_NewsletterService>("newsletterservice")
.WithReference(maildev);
builder.Build().Run();
更新 Program.cs 文件后,再次启动应用主机。 然后验证新闻稿服务是否已启动,并将环境变量 ConnectionStrings__maildev
添加到该过程。 在 资源 页中,找到 newsletterservice
行,然后选择 详细信息 列上的 视图 链接:
为 .NET.NET Aspire 仪表板中的新闻稿服务设置的环境变量
前面的屏幕截图显示了 newsletterservice
项目的环境变量。
ConnectionStrings__maildev
环境变量是由 maildev
资源注入到项目中的连接字符串。
使用连接字符串发送消息
若要使用注入到新闻稿服务项目中的 SMTP 连接详细信息,可将 SmtpClient 实例作为单一实例注入到依赖项注入容器中。 将以下代码添加到 Program.cs 文件中以设置 MailDevResource.NewsletterService 项目的单例服务。 在 Program
类中,紧跟 // Add services to the container
注释,添加以下代码:
builder.Services.AddSingleton<SmtpClient>(sp =>
{
var smtpUri = new Uri(builder.Configuration.GetConnectionString("maildev")!);
var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port);
return smtpClient;
});
提示
但是,此代码片段依赖于官方 SmtpClient
;此类型在某些平台上已过时,不建议将其用于其他平台。 有关使用 MailKit的更新式方法,请参阅 创建自定义 .NET Aspireclient 集成。
若要测试 client,请将两个简单的 subscribe
和 unsubscribe
POST 方法添加到新闻稿服务。 在 Program.cs 文件中添加以下代码,替换 MailDevResource.NewsletterService 项目中的 “weatherforecast” MapGet
调用,以设置 ASP.NET Core 路由:
app.MapPost("/subscribe", async (SmtpClient smtpClient, string email) =>
{
using var message = new MailMessage("newsletter@yourcompany.com", email)
{
Subject = "Welcome to our newsletter!",
Body = "Thank you for subscribing to our newsletter!"
};
await smtpClient.SendMailAsync(message);
});
app.MapPost("/unsubscribe", async (SmtpClient smtpClient, string email) =>
{
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 smtpClient.SendMailAsync(message);
});
提示
如果代码编辑器不自动添加这些命名空间,请记得在 Program.cs 中引用 System.Net.Mail
和 Microsoft.AspNetCore.Mvc
命名空间。
更新 Program.cs 文件后,启动应用程序主机,并使用浏览器或 curl
访问以下 URL(如果使用 Visual Studio,也可以使用 .http
文件):
POST /subscribe?email=test@test.com HTTP/1.1
Host: localhost:7251
Content-Type: application/json
若要使用此 API,可以使用 curl
发送请求。 以下 curl
命令向 subscribe
终结点发送 HTTP POST
请求,并且需要一个 email
查询字符串值来订阅新闻稿。
Content-Type
标头设置为 application/json
,以指示请求正文采用 JSON 格式。:
curl -H "Content-Type: application/json" --request POST https://localhost:7251/subscribe?email=test@test.com
下一个 API 是 unsubscribe
端点。 此终结点用于取消订阅新闻稿。
POST /unsubscribe?email=test@test.com HTTP/1.1
Host: localhost:7251
Content-Type: application/json
若要取消订阅新闻稿,可以使用以下 curl
命令,将 email
参数作为查询字符串传递到 unsubscribe
终结点:
curl -H "Content-Type: application/json" --request POST https://localhost:7251/unsubscribe?email=test@test.com
提示
请确保将 https://localhost:7251
替换为正确的 localhost 端口(正在运行的应用主机的 URL)。
如果这些 API 调用返回成功的响应(HTTP 200,正常),则您应该能够在资源 maildev
上选择仪表板,并且 MailDev UI 将显示已发送到 SMTP 终结点的电子邮件。
技术详细信息
在以下各节中,将讨论各种技术详细信息,这些细节对于开发 .NET.NET Aspire的自定义资源非常重要。
安全网络
在此示例中,MailDev 资源是通过 HTTP 和 SMTP 向主机公开的容器资源。 MailDev 资源是一种开发工具,不适用于生产用途。 若要改用 HTTPS,请参阅 MailDev:配置 HTTPS。
开发公开网络终结点的自定义资源时,请务必考虑资源的安全影响。 例如,如果资源是数据库,请务必确保数据库安全且连接字符串不会公开到公共 Internet。
ReferenceExpression
和 EndpointReference
类型
在前面的代码中,MailDevResource
有两个属性:
-
SmtpEndpoint
:EndpointReference 类型。 -
ConnectionStringExpression
:ReferenceExpression 类型。
这些类型在 .NET Aspire 中用于表示配置数据,只有在运行或通过如 Azure Developer CLI(azd
)这样的工具将 .NET Aspire 项目发布到云端时,配置数据才会最终确定。
这些类型帮助解决的基本问题是推迟解决具体配置信息,直到 所有 信息都可用。
例如,MailDevResource
根据 IResourceWithConnectionString 接口的要求公开名为 ConnectionStringExpression
的属性。 该属性的类型是 ReferenceExpression,并通过将插值字符串传入 Create 方法来创建。
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"smtp://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)}"
);
Create 方法的签名如下所示:
public static ReferenceExpression Create(
in ExpressionInterpolatedStringHandler handler)
这不是一个常规的 String 参数。 该方法利用 内插字符串处理程序模式,捕获内插字符串模板及其中引用的值,以允许进行自定义处理。 对于 .NET.NET Aspire,这些详细信息是在 ReferenceExpression 中捕获的,可以在内插字符串中引用的每个值都可用时进行评估。
下面是执行流的工作原理:
- 将实现 IResourceWithConnectionString 的资源添加到模型(例如,
AddMailDev(...)
)。 -
IResourceBuilder<MailDevResource>
传递给具有特殊重载的 WithReference,用于处理 IResourceWithConnectionString 实现器。 -
WithReference
将资源包装在 ConnectionStringReference 实例中,并在生成并开始运行 .NET.NET Aspire 项目后在 EnvironmentCallbackAnnotation 中捕获该对象。 - 引用连接字符串的进程开始时,.NET.NET Aspire 开始计算表达式。 它首先获取 ConnectionStringReference,然后调用 IValueProvider.GetValueAsync。
-
GetValueAsync
方法获取 ConnectionStringExpression 属性的值以获取 ReferenceExpression 实例。 - 然后,IValueProvider.GetValueAsync 方法调用 GetValueAsync 来处理以前捕获的内插字符串。
- 由于插值字符串包含对其他引用类型(例如 EndpointReference)的引用,它们也会被求值,并用实际值替换(此时现在可用)。
清单发布
IManifestExpressionProvider 接口旨在解决在部署时共享资源之间的连接信息的问题。 此特定问题的解决方法在 .NET.NET Aspire 内部循环网络概述中介绍。 与本地开发类似,许多值是配置应用所必需的,但在通过工具(如 azd
(Azure Developer CLI)部署应用之前,无法确定这些值。
若要解决此问题,.NET.NET Aspire 生成一个由 azd
和其他部署工具解释的清单文件。 部署工具使用表达式语法来评估资源之间的连接信息,而不是直接指定具体值。 通常,清单文件对开发人员不可见,但可以生成一个用于手动检查的文件。 以下命令可用于应用主机上生成清单。
dotnet run --project MailDevResource.AppHost/MailDevResource.AppHost.csproj -- --publisher manifest --output-path aspire-manifest.json
此命令生成如下所示的清单文件:
{
"resources": {
"maildev": {
"type": "container.v0",
"connectionString": "smtp://{maildev.bindings.smtp.host}:{maildev.bindings.smtp.port}",
"image": "docker.io/maildev/maildev:2.1.0",
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 1080
},
"smtp": {
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 1025
}
}
},
"newsletterservice": {
"type": "project.v0",
"path": "../MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj",
"env": {
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"ConnectionStrings__maildev": "{maildev.connectionString}"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http"
},
"https": {
"scheme": "https",
"protocol": "tcp",
"transport": "http"
}
}
}
}
}
由于 MailDevResource
实现 IResourceWithConnectionString.NET.NET Aspire 中的清单发布逻辑知道,即使 MailDevResource
是容器资源,它也需要 connectionString
字段。
connectionString
字段引用清单中 maildev
资源的其他部分,以生成最终字符串:
{
// ... other content omitted.
"connectionString": "smtp://{maildev.bindings.smtp.host}:{maildev.bindings.smtp.port}"
}
.NET .NET Aspire 知道如何形成此字符串,因为它查看 ConnectionStringExpression 并通过 IManifestExpressionProvider 接口构建最终字符串(与使用 IValueProvider 接口的方式大致相同)。
MailDevResource
自动包含在清单中,因为它派生自 ContainerResource。 资源作者可以选择通过在资源生成器上使用 ExcludeFromManifest 扩展方法禁止将内容输出到清单。
public static IResourceBuilder<MailDevResource> AddMailDev(
this IDistributedApplicationBuilder builder,
string name,
int? httpPort = null,
int? smtpPort = null)
{
var resource = new MailDevResource(name);
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)
.ExcludeFromManifest(); // This line was added
}
应仔细考虑资源是否应出现在清单中,还是应省略。 如果将资源添加到清单中,则应以安全可靠的方式进行配置。
总结
在自定义资源教程中,你学习了如何创建自定义 .NET Aspire 资源,该资源使用现有的容器化应用程序(MailDev)。 然后,通过轻松测试应用内可能使用的电子邮件功能来改进本地开发体验。 这些学习可以应用于构建可在基于 .NET.NET Aspire的应用程序中使用的其他自定义资源。 此特定示例不包含任何自定义集成,但可以生成自定义集成,以便开发人员更轻松地使用资源。 在此情境中,你能够依赖 .NET 平台中的现有 SmtpClient
类发送电子邮件。