Skip to content

Create custom .NET Aspire component #1355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions docs/extensibility/custom-component.md

Large diffs are not rendered by default.

54 changes: 46 additions & 8 deletions docs/extensibility/custom-resources.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
title: Create custom resource types for .NET Aspire
description: Learn how to create a custom resource for an existing containerized application.
ms.date: 05/29/2024
ms.date: 07/15/2024
ms.topic: how-to
ms.custom: devx-track-extended-azdevcli
---

# Create custom resource types for .NET Aspire
Expand Down Expand Up @@ -230,7 +229,7 @@ In order to test the end-to-end scenario, you need a .NET project which we can i
1. Create a new .NET project named _:::no-loc text="MailDevResource.NewsletterService":::_.

```dotnetcli
dotnet new webapi --use-minimal-apis --no-openapi -o MailDevResource.NewsletterService
dotnet new webapi --use-minimal-apis -o MailDevResource.NewsletterService
```

1. Add a reference to the _:::no-loc text="MailDev.Hosting":::_ project.
Expand Down Expand Up @@ -265,11 +264,45 @@ The preceding screenshot shows the environment variables for the `newsletterserv

To use the SMTP connection details that were injected into the newsletter service project, you inject an instance of <xref:System.Net.Mail.SmtpClient> into the dependency injection container as a singleton. Add the following code to the _:::no-loc text="Program.cs":::_ file in the _:::no-loc text="MailDevResource.NewsletterService":::_ project to setup the singleton service. In the `Program` class, immediately following the `// Add services to the container` comment, add the following code:

:::code source="snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs" id="smtp":::
```csharp
builder.Services.AddSingleton<SmtpClient>(sp =>
{
var smtpUri = new Uri(builder.Configuration.GetConnectionString("maildev")!);

To test the client, add two simple `subscribe` and `unsubscribe` GET methods to the newsletter service. Add the following code after the `MapGet` call in the _:::no-loc text="Program.cs":::_ file of the _MailDevResource.NewsletterService_ project to setup the ASP.NET Core routes:
var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port);

:::code source="snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs" id="subs":::
return smtpClient;
});
```

> [!TIP]
> This code snippet relies on the official `SmtpClient`, however; this type is obsolete on some platforms and not recommended on others. This is used here to demonstrate a non-componentized approach to using the MailDev resource. For a more modern approach using [MailKit](https://github.com/jstedfast/MailKit), see [Create custom .NET Aspire component](custom-component.md).

To test the client, add two simple `subscribe` and `unsubscribe` POST methods to the newsletter service. Add the following code replacing the "weatherforecast" `MapGet` call in the _:::no-loc text="Program.cs":::_ file of the _MailDevResource.NewsletterService_ project to setup the ASP.NET Core routes:

```csharp
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);
});
```

> [!TIP]
> Remember to reference the `System.Net.Mail` and `Microsoft.AspNetCore.Mvc` namespaces in _:::no-loc text="Program.cs":::_ if your code editor doesn't automatically add them.
Expand All @@ -293,7 +326,7 @@ curl -H "Content-Type: application/json" --request POST https://localhost:7251/s
## [Windows](#tab/windows)

```powershell
curl -H "Content-Type: application/json" --request POST https://localhost:7251/subscribe?email=test@test.com
curl -H @{ ContentType = "application/json" } -Method POST https://localhost:7251/subscribe?email=test@test.com
```

---
Expand All @@ -317,7 +350,7 @@ curl -H "Content-Type: application/json" --request POST https://localhost:7251/u
## [Windows](#tab/windows)

```powershell
curl -H "Content-Type: application/json" --request POST https://localhost:7251/unsubscribe?email=test@test.com
curl -H @{ ContentType = "application/json" } -Method POST https://localhost:7251/unsubscribe?email=test@test.com
```

---
Expand Down Expand Up @@ -482,3 +515,8 @@ Careful consideration should be given as to whether the resource should be prese
## Summary

In the custom resource tutorial, you learned how to create a custom .NET Aspire resource which uses an existing containerized application (MailDev). You then used that to improve the local development experience by making it easy to test e-mail capabilities that might be used within an app. These learnings can be applied to building out other custom resources that can be used in .NET Aspire-based applications. This specific example didn't include any custom components, but it's possible to build out custom components to make it easier for developers to use the resource. In this scenario you were able to rely on the existing `SmtpClient` class in the .NET platform to send e-mails.

## Next steps

> [!div class="nextstepaction"]
> [Create custom .NET Aspire component](custom-component.md)
Binary file added docs/extensibility/media/maildev-inbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/extensibility/media/swagger-ui-try.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/extensibility/media/swagger-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MailKit.Client\MailKit.Client.csproj" />
<ProjectReference Include="..\MailDev.Hosting\MailDev.Hosting.csproj" />
<ProjectReference Include="..\MailDevResource.ServiceDefaults\MailDevResource.ServiceDefaults.csproj" />
</ItemGroup>
Expand All @@ -9,6 +15,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>f2608916-3fcc-4bd8-9697-ce27b4156fc0</UserSecretsId>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,75 +1,54 @@
using System.Net.Mail;
using Microsoft.AspNetCore.Mvc;
using MailKit.Client;
using MailKit.Net.Smtp;
using MimeKit;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Add services to the container.
// <smtp>
builder.Services.AddSingleton<SmtpClient>(sp =>
{
var smtpUri = new Uri(builder.Configuration.GetConnectionString("maildev")!);

var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

return smtpClient;
});
// </smtp>
// 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();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
app.MapPost("/subscribe",
async (MailKitClientFactory factory, string email) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
ISmtpClient client = await factory.GetSmtpClientAsync();

// <subs>
app.MapPost("/subscribe", async ([FromServices] 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);
await client.SendAsync(MimeMessage.CreateFromMailMessage(message));
});

app.MapPost("/unsubscribe", async ([FromServices] SmtpClient smtpClient, string email) =>
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 smtpClient.SendMailAsync(message);
await client.SendAsync(MimeMessage.CreateFromMailMessage(message));
});
// </subs>

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:21371",
"sslPort": 44378
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5021",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:7251;http://localhost:5021",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7251;http://localhost:5021",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
13 changes: 10 additions & 3 deletions docs/extensibility/snippets/MailDevResource/MailDevResource.sln
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
Microsoft Visual Studio Solution File, Format Version 12.00

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.0.0
MinimumVisualStudioVersion = 17.8.0.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDevResource.AppHost", "MailDevResource.AppHost\MailDevResource.AppHost.csproj", "{70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDevResource.ServiceDefaults", "MailDevResource.ServiceDefaults\MailDevResource.ServiceDefaults.csproj", "{9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailDev.Hosting", "MailDev.Hosting\MailDev.Hosting.csproj", "{896B2FA6-E580-4AFC-ACC5-383D052F9EEB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDev.Hosting", "MailDev.Hosting\MailDev.Hosting.csproj", "{896B2FA6-E580-4AFC-ACC5-383D052F9EEB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailDevResource.NewsletterService", "MailDevResource.NewsletterService\MailDevResource.NewsletterService.csproj", "{3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDevResource.NewsletterService", "MailDevResource.NewsletterService\MailDevResource.NewsletterService.csproj", "{3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailKit.Client", "MailKit.Client\MailKit.Client.csproj", "{C9A58484-13CC-4391-9D93-88FDFA7B0DDF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -32,6 +35,10 @@ Global
{3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}.Release|Any CPU.Build.0 = Release|Any CPU
{C9A58484-13CC-4391-9D93-88FDFA7B0DDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9A58484-13CC-4391-9D93-88FDFA7B0DDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9A58484-13CC-4391-9D93-88FDFA7B0DDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9A58484-13CC-4391-9D93-88FDFA7B0DDF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Resilience" Version="8.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.7" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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>
/// <param name="credentials">
/// The optional <see cref="ICredentials"/> used to authenticate to 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();
}
}
Loading
Loading