diff --git a/docs/extensibility/custom-component.md b/docs/extensibility/custom-component.md index 98c563239c..d03c75ecf8 100644 --- a/docs/extensibility/custom-component.md +++ b/docs/extensibility/custom-component.md @@ -49,12 +49,11 @@ The next step is to add all the NuGet packages that the component relies on. Rat Whenever you're creating a .NET Aspire component, it's best to understand the client library that you're mapping to. With MailKit, you need to understand the configuration settings that are required to connect to a Simple Mail Transfer Protocol (SMTP) server. But it's also important to understand if the library has support for _health checks_, _tracing_ and _metrics_. MailKit supports _tracing_ and _metrics_, through its [`Telemetry.SmtpClient` class](https://github.com/jstedfast/MailKit/blob/master/MailKit/Telemetry.cs#L112-L189). When adding _health checks_, you should use any established or existing health checks where possible. Otherwise, you might consider implementing your own in the component. Add the following code to the `MailKit.Client` project in a file named _MailKitClientSettings.cs_: -:::code source="snippets/MailDevResource/MailKit.Client/MailKitClientSettings.cs"::: +:::code source="snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientSettings.cs"::: The preceding code defines the `MailKitClientSettings` class with: - `Endpoint` property that represents the connection string to the SMTP server. -- `Credentials` property that represents the credentials to authenticate with the SMTP server. - `DisableHealthChecks` property that determines whether health checks are enabled. - `DisableTracing` property that determines whether tracing is enabled. - `DisableMetrics` property that determines whether metrics are enabled. @@ -64,35 +63,26 @@ The preceding code defines the `MailKitClientSettings` class with: The settings class also contains a `ParseConnectionString` method that parses the connection string into a valid `Uri`. The configuration is expected to be provided in the following format: - `ConnectionStrings:`: The connection string to the SMTP server. -- `MailKit:Client:Endpoint`: The connection string to the SMTP server. +- `MailKit:Client:ConnectionString`: The connection string to the SMTP server. -If neither of these values are provided, an exception is thrown. Likewise, if there's a value but it's not a valid URI, an exception is thrown. +If neither of these values are provided, an exception is thrown. -### Parse credentials logic - -The settings class also contains a `ParseCredentials` method that parses the credentials into a valid `NetworkCredential`. The configuration is expected to be provided in the following format: - -- `MailKit:Client:Credentials:UserName`: The username to authenticate with the SMTP server. -- `MailKit:Client:Credentials:Password`: The password to authenticate with the SMTP server. - -When credentials are configured, the `ParseCredentials` method attempts to parse the username and password from the configuration. If either the username or password is missing, an exception is thrown. - -## Expose component wrapper functionality +## Expose client functionality The goal of .NET Aspire components is to expose the underlying client library to consumers through dependency injection. With MailKit and for this example, the `SmtpClient` class is what you want to expose. You're not wrapping any functionality, but rather mapping configuration settings to an `SmtpClient` class. It's common to expose both standard and keyed-service registrations for components. Standard registrations are used when there's only one instance of a service, and keyed-service registrations are used when there are multiple instances of a service. Sometimes, to achieve multiple registrations of the same type you use a factory pattern. Add the following code to the `MailKit.Client` project in a file named _MailKitClientFactory.cs_: -:::code source="snippets/MailDevResource/MailKit.Client/MailKitClientFactory.cs"::: +:::code source="snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientFactory.cs"::: -The `MailKitClientFactory` class is a factory that creates an `ISmtpClient` instance based on the configuration settings. It's responsible for returning an `ISmtpClient` implementation that has an active connection to a configured SMTP server and optionally authenticated. Next, you need to expose the functionality for the consumers to register this factory with the dependency injection container. Add the following code to the `MailKit.Client` project in a file named _MailKitExtensions.cs_: +The `MailKitClientFactory` class is a factory that creates an `ISmtpClient` instance based on the configuration settings. It's responsible for returning an `ISmtpClient` implementation that has an active connection to a configured SMTP server. Next, you need to expose the functionality for the consumers to register this factory with the dependency injection container. Add the following code to the `MailKit.Client` project in a file named _MailKitExtensions.cs_: -:::code source="snippets/MailDevResource/MailKit.Client/MailKitExtensions.cs"::: +:::code source="snippets/MailDevResourceAndComponent/MailKit.Client/MailKitExtensions.cs"::: The preceding code adds two extension methods on the `IHostApplicationBuilder` type, one for the standard registration of MailKit and another for keyed-registration of MailKit. > [!TIP] > Extension methods for .NET Aspire components should extend the `IHostApplicationBuilder` type and follow the `Add` naming convention where the `` is the type or functionality you're adding. For this article, the `AddMailKitClient` extension method is used to add the MailKit client. It's likely more in-line with the official guidance to use `AddMailKitSmtpClient` instead of `AddMailKitClient`, since this only registers the `SmtpClient` and not the entire MailKit library. -Both extensions ultimately rely on the private `AddMailKitClient` method to register the `MailKitClientFactory` with the dependency injection container as a [scoped service](/dotnet/core/extensions/dependency-injection#scoped). The reason for registering the `MailKitClientFactory` as a scoped service is because the connection (and authentication) operations are considered expensive and should be reused within the same scope where possible. In other words, for a single request, the same `ISmtpClient` instance should be used. The factory holds on to the instance of the `SmtpClient` that it creates and disposes of it. +Both extensions ultimately rely on the private `AddMailKitClient` method to register the `MailKitClientFactory` with the dependency injection container as a [scoped service](/dotnet/core/extensions/dependency-injection#scoped). The reason for registering the `MailKitClientFactory` as a scoped service is because the connection operations are considered expensive and should be reused within the same scope where possible. In other words, for a single request, the same `ISmtpClient` instance should be used. The factory holds on to the instance of the `SmtpClient` that it creates and disposes of it. ### Configuration binding @@ -107,7 +97,7 @@ The registration of health checks, and telemetry are described in a bit more det [Health checks](../fundamentals/health-checks.md) are a way to monitor the health of a component. With MailKit, you can check if the connection to the SMTP server is healthy. Add the following code to the `MailKit.Client` project in a file named _MailKitHealthCheck.cs_: -:::code source="snippets/MailDevResource/MailKit.Client/MailKitHealthCheck.cs"::: +:::code source="snippets/MailDevResourceAndComponent/MailKit.Client/MailKitHealthCheck.cs"::: The preceding health check implementation: @@ -165,9 +155,9 @@ With the component library created, you can now update the Newsletter service to dotnet add ./MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj reference MailKit.Client/MailKit.Client.csproj ``` -The final step is to replace the existing _Program.cs_ file in the `MailDevResource.NewsletterService` project with the following C# code: +The final step is to replace the existing _:::no-loc text="Program.cs":::_ file in the `MailDevResource.NewsletterService` project with the following C# code: -:::code source="snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs"::: +:::code source="snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Program.cs"::: The most notable changes in the preceding code are: @@ -195,59 +185,6 @@ Repeat this several times, to add multiple email addresses. You should see the e Stop the application by selecting Ctrl+C in the terminal window where the application is running, or by selecting the stop button in your IDE. -### Configure MailDev credentials - -The MailDev container supports basic authentication for both incoming and outgoing SMTP. To configure the credentials for incoming, you need to set the `MAILDEV_INCOMING_USER` and `MAILDEV_INCOMING_PASS` environment variables. For more information, see [MailDev: Usage](https://maildev.github.io/maildev/#usage). - -To configure these credentials, update the _Program.cs_ file in the `MailDevResource.AppHost` project with the following code: - -```csharp -var builder = DistributedApplication.CreateBuilder(args); - -var mailDevUsername = builder.AddParameter("maildev-username"); -var mailDevPassword = builder.AddParameter("maildev-password"); - -var maildev = builder.AddMailDev("maildev") - .WithEnvironment("MAILDEV_INCOMING_USER", mailDevUsername) - .WithEnvironment("MAILDEV_INCOMING_PASS", mailDevPassword); - -builder.AddProject("newsletterservice") - .WithReference(maildev); - -builder.Build().Run(); -``` - -The preceding code adds two parameters for the MailDev username and password. It assigns these parameters to the `MAILDEV_INCOMING_USER` and `MAILDEV_INCOMING_PASS` environment variables. The `AddMailDev` method has two chained calls to `WithEnvironment` which includes these environment variables. For more information on parameters, see [External parameters](../fundamentals/external-parameters.md). - -Next, configure the secrets for these paremeters. Right-click on the `MailDevResource.AppHost` project and select `Manage User Secrets`. Add the following JSON to the `secrets.json` file: - -```json -{ - "Parameters:maildev-username": "@admin", - "Parameters:maildev-password": "t3st1ng" -} -``` - -> [!WARNING] -> These credentials are for demonstration purposes only and MailDev is intended for local development. These crednetials are fictitious and shouldn't be used in a production environment. - -If you're to run the sample now, the client wouldn't be able to connect to the MailDev container. This is because the MailDev container is configured to require authentication for incoming SMTP connections. The MailKit client configuration also needs to be updated to include the credentials. - -To configure the credentials in the client, right-click on the `MailDevResource.NewsletterService` project and select `Manage User Secrets`. Add the following JSON to the `secrets.json` file: - -```json -{ - "MailKit:Client": { - "Credentials": { - "UserName": "@admin", - "Password": "t3st1ng" - } - } -} -``` - -Run the app again, and everything works as it did before, but now with authentication enabled. - ### View MailKit telemetry The MailKit client library exposes telemetry that can be viewed in the .NET Aspire dashboard. To view the telemetry, navigate to the .NET Aspire dashboard at [https://localhost:7251](https://localhost:7251). Select the `newsletter` resource to view the telemetry on the **Metrics** page: @@ -263,3 +200,8 @@ Open up the Swagger UI again, and make some requests to the `/subscribe` and `/u In this article, you learned how to create a .NET Aspire component that uses MailKit to send emails. You also learned how to integrate this component into the Newsletter app you previously built. You learned about the core principles of .NET Aspire components, such as exposing the underlying client library to consumers through dependency injection, and how to add health checks and telemetry to the component. You also learned how to update the Newsletter service to use the MailKit client. Go forth and build your own .NET Aspire components. If you believe that there's enough community value in the component you're building, consider publishing it as a [NuGet package](/dotnet/standard/library-guidance/nuget) for others to use. Furthermore, consider submitting a pull request to the [.NET Aspire GitHub repository](https://github.com/dotnet/aspire) for consideration to be included in the official .NET Aspire components. + +## Next steps + +> [!div class="nextstepaction"] +> [Implement auth from custom resource to component](implement-auth-from-resource-to-component.md) diff --git a/docs/extensibility/custom-resources.md b/docs/extensibility/custom-resources.md index 8c4ac58bc0..2aae26473f 100644 --- a/docs/extensibility/custom-resources.md +++ b/docs/extensibility/custom-resources.md @@ -1,7 +1,7 @@ --- title: Create custom resource types for .NET Aspire description: Learn how to create a custom resource for an existing containerized application. -ms.date: 07/15/2024 +ms.date: 07/17/2024 ms.topic: how-to --- @@ -264,45 +264,14 @@ 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 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: -```csharp -builder.Services.AddSingleton(sp => -{ - var smtpUri = new Uri(builder.Configuration.GetConnectionString("maildev")!); - - var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port); - - return smtpClient; -}); -``` +:::code source="snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs" id="smtp"::: > [!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); -}); -``` +:::code source="snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs" id="subs"::: > [!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. diff --git a/docs/extensibility/implement-auth-from-resource-to-component.md b/docs/extensibility/implement-auth-from-resource-to-component.md new file mode 100644 index 0000000000..0beb2f667a --- /dev/null +++ b/docs/extensibility/implement-auth-from-resource-to-component.md @@ -0,0 +1,94 @@ +--- +title: Implement auth from custom resource to component +description: Learn how to implement authentication credentials from a custom resource to a custom component. +ms.date: 07/16/2024 +ms.topic: how-to +--- + +# Implement auth from custom resource to component + +This article is a continuation of two previous articles: + +- [Create custom resource types for .NET Aspire](custom-resources.md) +- [Create custom .NET Aspire component](custom-component.md) + +One of the primary benefits to .NET Aspire is how it simplifies the configurability of resources and consuming clients (or components). This article demonstrates how to authentication credentials from from a custom resource to a custom component. The custom resource is a MailDev container that allows for either incoming or outgoing credentials. The custom component is a MailKit client that sends emails. + +## Prerequisites + +Since this article continues from previous content, it's expected that you've already created the resulting solution as a starting point for this article. If you haven't already, complete the following articles: + +- [Create custom resource types for .NET Aspire](custom-resources.md) +- [Create custom .NET Aspire component](custom-component.md) + +The resulting solution from these previous articles contains the following projects: + +- _MailDev.Hosting_: Contains the custom resource type for the MailDev container. +- _MailDevResource.AppHost_: The [app host](../fundamentals/app-host-overview.md) that uses the custom resource and defines it as a dependency for a Newsletter service. +- _MailDevResource.NewsletterService_: An ASP.NET Core Web API project that sends emails using the MailDev container. +- _MailDevResource.ServiceDefaults_: Contains the [default service configurations](../fundamentals/service-defaults.md) intended for sharing. +- _MailKit.Client_: Contains the custom component that exposes the MailKit `SmptClient` through a factory. + +## Update the MailDev resource + +To flow authentication credentials from the MailDev resource to the MailKit component, you need to update the MailDev resource to include the username and password parameters. + +The MailDev container supports basic authentication for both incoming and outgoing SMTP. To configure the credentials for incoming, you need to set the `MAILDEV_INCOMING_USER` and `MAILDEV_INCOMING_PASS` environment variables. For more information, see [MailDev: Usage](https://maildev.github.io/maildev/#usage). Update the _MailDevResource.cs_ file in the `MailDev.Hosting` project, by replacing its contents with the following C# code: + +:::code source="snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResource.cs" highlight="9-10"::: + +These updates add a `UsernameParameter` and `PasswordParameter` property. These properties are used to store the parameters for the MailDev username and password. The `ConnectionStringExpression` property is updated to include the username and password parameters in the connection string. Next, update the _MailDevResourceBuilderExtensions.cs_ file in the `MailDev.Hosting` project with the following C# code: + +:::code source="snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResourceBuilderExtensions.cs" highlight="9-10,29-30,32-34,40-41,55-59"::: + +The preceding code updates the `AddMailDev` extension method to include the `userName` and `password` parameters. The `WithEnvironment` method is updated to include the `UserEnvVarName` and `PasswordEnvVarName` environment variables. These environment variables are used to set the MailDev username and password. + +## Update the app host + +Now that the resource is updated to include the username and password parameters, you need to update the app host to include these parameters. Update the _:::no-loc text="Program.cs":::_ file in the `MailDevResource.AppHost` project with the following C# code: + +:::code source="snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Program.cs" highlight="3-4,6-9"::: + +The preceding code adds two parameters for the MailDev username and password. It assigns these parameters to the `MAILDEV_INCOMING_USER` and `MAILDEV_INCOMING_PASS` environment variables. The `AddMailDev` method has two chained calls to `WithEnvironment` which includes these environment variables. For more information on parameters, see [External parameters](../fundamentals/external-parameters.md). + +Next, configure the secrets for these paremeters. Right-click on the `MailDevResource.AppHost` project and select `Manage User Secrets`. Add the following JSON to the `secrets.json` file: + +```json +{ + "Parameters:maildev-username": "@admin", + "Parameters:maildev-password": "t3st1ng" +} +``` + +> [!WARNING] +> These credentials are for demonstration purposes only and MailDev is intended for local development. These crednetials are fictitious and shouldn't be used in a production environment. + +## Update the MailKit component + +It's good practice for components to expect connection strings to contain varions key/value pairs, and to parse these pairs into the appropriate properties. Update the _MailKitClientSettings.cs_ file in the `MailKit.Client` project with the following C# code: + +:::code source="snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientSettings.cs" highlight="21-28,95-100"::: + +The preceding settings class, now includes a `Credentials` property of type `NetworkCredential`. The `ParseConnectionString` method is updated to parse the `Username` and `Password` keys from the connection string. If the `Username` and `Password` keys are present, a `NetworkCredential` is created and assigned to the `Credentials` property. + +With the settings class updated to understand and populate the credentials, update the factory to conditionally use the credentials if they're configured. Update the _MailKitClientFactory.cs_ file in the `MailKit.Client` project with the following C# code: + +:::code source="snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientFactory.cs" highlight="44-48"::: + +When the factory determines that credentials have been configured, it authenticates with the SMTP server after connecting before returning the `SmtpClient`. + +## Run the sample + +Now that you've updated both the resource and corresponding component projects, as well as the app host, you're ready to run the sample app. To run the sample from your IDE, select F5 or use `dotnet run` from the root directory of the solution to start the application—you should see the [.NET Aspire dashboard](../fundamentals/dashboard/overview.md). Navigate to the `maildev` container resource and view the details. You should see the username and password parameters in the resource details, under the **Environment Variables** section: + +:::image type="content" source="media/maildev-details.png" lightbox="media/maildev-details.png" alt-text=".NET Aspire Dashboard: MailDev container resource details."::: + +Likewise, you should see the connection string in the `newsletterservice` resource details, under the **Environment Variables** section: + +:::image type="content" source="media/newsletter-details.png" lightbox="media/newsletter-details.png" alt-text=".NET Aspire Dashboard: Newsletter service resource details."::: + +Validate that everything is working as expected. + +## Summary + +This article demonstrated how to flow authentication credentials from a custom resource to a custom component. The custom resource is a MailDev container that allows for either incoming or outgoing credentials. The custom component is a MailKit client that sends emails. By updating the resource to include the username and password parameters, and updating the component to parse and use these parameters, you can flow authentication credentials from the resource to the component. diff --git a/docs/extensibility/media/maildev-details.png b/docs/extensibility/media/maildev-details.png new file mode 100644 index 0000000000..02b3af6d71 Binary files /dev/null and b/docs/extensibility/media/maildev-details.png differ diff --git a/docs/extensibility/media/newsletter-details.png b/docs/extensibility/media/newsletter-details.png new file mode 100644 index 0000000000..fa8c536fb5 Binary files /dev/null and b/docs/extensibility/media/newsletter-details.png differ diff --git a/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj b/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj index 68f91ed347..6fdf0e9de2 100644 --- a/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj +++ b/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj @@ -6,7 +6,6 @@ - diff --git a/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs b/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs index 45b2365ccd..3b99f04dcb 100644 --- a/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs +++ b/docs/extensibility/snippets/MailDevResource/MailDevResource.NewsletterService/Program.cs @@ -1,7 +1,4 @@ using System.Net.Mail; -using MailKit.Client; -using MailKit.Net.Smtp; -using MimeKit; var builder = WebApplication.CreateBuilder(args); @@ -11,7 +8,16 @@ builder.Services.AddSwaggerGen(); // Add services to the container. -builder.AddMailKitClient("maildev"); +// +builder.Services.AddSingleton(sp => +{ + var smtpUri = new Uri(builder.Configuration.GetConnectionString("maildev")!); + + var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port); + + return smtpClient; +}); +// var app = builder.Build(); @@ -23,32 +29,28 @@ app.UseSwaggerUI(); app.UseHttpsRedirection(); -app.MapPost("/subscribe", - async (MailKitClientFactory factory, string email) => +// +app.MapPost("/subscribe", async (SmtpClient smtpClient, 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)); + await smtpClient.SendMailAsync(message); }); -app.MapPost("/unsubscribe", - async (MailKitClientFactory factory, string email) => +app.MapPost("/unsubscribe", async (SmtpClient smtpClient, 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)); + await smtpClient.SendMailAsync(message); }); +// app.Run(); diff --git a/docs/extensibility/snippets/MailDevResource/MailDevResource.sln b/docs/extensibility/snippets/MailDevResource/MailDevResource.sln index 88c6bd3078..932d4b427e 100644 --- a/docs/extensibility/snippets/MailDevResource/MailDevResource.sln +++ b/docs/extensibility/snippets/MailDevResource/MailDevResource.sln @@ -11,8 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDev.Hosting", "MailDev. EndProject 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 Debug|Any CPU = Debug|Any CPU @@ -35,10 +33,6 @@ 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 diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDev.Hosting.csproj b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDev.Hosting.csproj new file mode 100644 index 0000000000..fc602e5bc4 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDev.Hosting.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResource.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResource.cs new file mode 100644 index 0000000000..2e76605b94 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResource.cs @@ -0,0 +1,30 @@ +// 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)}" + ); +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResourceBuilderExtensions.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResourceBuilderExtensions.cs new file mode 100644 index 0000000000..c13c3269e5 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDev.Hosting/MailDevResourceBuilderExtensions.cs @@ -0,0 +1,57 @@ +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 +{ + /// + /// Adds the to the given + /// instance. Uses the "2.0.2" tag. + /// + /// The . + /// The name of the resource. + /// The HTTP port. + /// The SMTP port. + /// + /// An instance that + /// represents the added MailDev resource. + /// + public static IResourceBuilder 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 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.0.2"; +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/MailDevResource.AppHost.csproj b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/MailDevResource.AppHost.csproj new file mode 100644 index 0000000000..4e177d9495 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/MailDevResource.AppHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + 9c9bfb14-6706-4421-bc93-37cbaebe36d0 + + + + + + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Program.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Program.cs new file mode 100644 index 0000000000..49c9ab4df3 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Program.cs @@ -0,0 +1,8 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var maildev = builder.AddMailDev("maildev"); + +builder.AddProject("newsletterservice") + .WithReference(maildev); + +builder.Build().Run(); diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Properties/launchSettings.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..f0583ee1d3 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17251;http://localhost:15199", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21161", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22175" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15199", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19277", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20014" + } + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.Development.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj new file mode 100644 index 0000000000..68f91ed347 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + net8.0 + enable + enable + f2608916-3fcc-4bd8-9697-ce27b4156fc0 + + + diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http new file mode 100644 index 0000000000..39958c6813 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http @@ -0,0 +1,6 @@ +@MailDevResource.NewsletterService_HostAddress = http://localhost:5021 + +GET {{MailDevResource.NewsletterService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Program.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Program.cs new file mode 100644 index 0000000000..45b2365ccd --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Program.cs @@ -0,0 +1,54 @@ +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(); diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Properties/launchSettings.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Properties/launchSettings.json new file mode 100644 index 0000000000..aed106ab5c --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7251;http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.Development.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.json b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.NewsletterService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/Extensions.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000000..b6e80000de --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/Extensions.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // builder.Services.AddOpenTelemetry() + // .WithMetrics(metrics => metrics.AddPrometheusExporter()); + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // app.MapPrometheusScrapingEndpoint(); + } + + return app; + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj new file mode 100644 index 0000000000..b8d847c913 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResourceAndComponent.sln b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResourceAndComponent.sln new file mode 100644 index 0000000000..88c6bd3078 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailDevResourceAndComponent.sln @@ -0,0 +1,49 @@ + +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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDev.Hosting", "MailDev.Hosting\MailDev.Hosting.csproj", "{896B2FA6-E580-4AFC-ACC5-383D052F9EEB}" +EndProject +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 + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Release|Any CPU.Build.0 = Release|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Release|Any CPU.Build.0 = Release|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Release|Any CPU.Build.0 = Release|Any CPU + {3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D40F9870-8B8A-4331-BA52-CB368EDD31D6} + EndGlobalSection +EndGlobal diff --git a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKit.Client.csproj b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKit.Client.csproj similarity index 100% rename from docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKit.Client.csproj rename to docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKit.Client.csproj diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientFactory.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientFactory.cs new file mode 100644 index 0000000000..309dcc13c8 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientFactory.cs @@ -0,0 +1,57 @@ +using MailKit.Net.Smtp; + +namespace MailKit.Client; + +/// +/// A factory for creating instances +/// given a (and optional ). +/// +/// +/// The settings for the SMTP server +/// +public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable +{ + private readonly SemaphoreSlim _semaphore = new(1, 1); + + private SmtpClient? _client; + + /// + /// Gets an instance in the connected state + /// (and that's been authenticated if configured). + /// + /// Used to abort client creation and connection. + /// A connected (and authenticated) instance. + /// + /// Since both the connection and authentication are considered expensive operations, + /// the returned is intended to be used for the duration of a request + /// (registered as 'Scoped') and is automatically disposed of. + /// + public async Task 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(); + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientSettings.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientSettings.cs new file mode 100644 index 0000000000..a68c5b0dff --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitClientSettings.cs @@ -0,0 +1,86 @@ +using System.Data.Common; + +namespace MailKit.Client; + +/// +/// Provides the client configuration settings for connecting MailKit to an SMTP server. +/// +public sealed class MailKitClientSettings +{ + internal const string DefaultConfigSectionName = "MailKit:Client"; + + /// + /// Gets or sets the SMTP server . + /// + /// + /// The default value is . + /// + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the database health check is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableTracing { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are disabled or not. + /// + /// + /// The default value is . + /// + 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:' + 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:' (or 'Endpoint' key in + '{DefaultConfigSectionName}') is missing. + """); + } + + if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var uri) is false) + { + throw new InvalidOperationException($""" + The 'ConnectionStrings:' (or 'Endpoint' key in + '{DefaultConfigSectionName}') isn't a valid URI. + """); + } + + Endpoint = uri; + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitExtensions.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitExtensions.cs new file mode 100644 index 0000000000..4e5388f60f --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitExtensions.cs @@ -0,0 +1,134 @@ +using MailKit; +using MailKit.Client; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Provides extension methods for registering a as a +/// scoped-lifetime service in the services provided by the . +/// +public static class MailKitExtensions +{ + /// + /// Registers 'Scoped' for creating + /// connected instance for sending emails. + /// + /// + /// The to read config from and add services to. + /// + /// + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// + /// + /// An optional delegate that can be used for customizing options. + /// It's invoked after the settings are read from the configuration. + /// + public static void AddMailKitClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) => + AddMailKitClient( + builder, + MailKitClientSettings.DefaultConfigSectionName, + configureSettings, + connectionName, + serviceKey: null); + + /// + /// Registers 'Scoped' for creating + /// connected instance for sending emails. + /// + /// + /// The to read config from and add services to. + /// + /// + /// The name of the component, which is used as the of the + /// service and also to retrieve the connection string from the ConnectionStrings configuration section. + /// + /// + /// An optional method that can be used for customizing options. It's invoked after the settings are + /// read from the configuration. + /// + public static void AddKeyedMailKitClient( + this IHostApplicationBuilder builder, + string name, + Action? 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? 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( + 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)); + } + } +} diff --git a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitHealthCheck.cs b/docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitHealthCheck.cs similarity index 100% rename from docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitHealthCheck.cs rename to docs/extensibility/snippets/MailDevResourceAndComponent/MailKit.Client/MailKitHealthCheck.cs diff --git a/docs/extensibility/snippets/MailDevResourceAndComponent/global.json b/docs/extensibility/snippets/MailDevResourceAndComponent/global.json new file mode 100644 index 0000000000..a1aaefd486 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceAndComponent/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.303", + "rollForward": "latestPatch" + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDev.Hosting.csproj b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDev.Hosting.csproj new file mode 100644 index 0000000000..fc602e5bc4 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDev.Hosting.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResource.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResource.cs new file mode 100644 index 0000000000..0ea11f37ce --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResource.cs @@ -0,0 +1,50 @@ +// 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; + + /// + /// Gets the parameter that contains the MailDev SMTP server username. + /// + public ParameterResource? UsernameParameter { get; } = username; + + internal ReferenceExpression UserNameReference => + UsernameParameter is not null ? + ReferenceExpression.Create($"{UsernameParameter}") : + ReferenceExpression.Create($"{DefaultUsername}"); + + /// + /// Gets the parameter that contains the MailDev SMTP server password. + /// + 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}" + ); +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResourceBuilderExtensions.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResourceBuilderExtensions.cs new file mode 100644 index 0000000000..99a9e81a67 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDev.Hosting/MailDevResourceBuilderExtensions.cs @@ -0,0 +1,72 @@ +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"; + + /// + /// Adds the to the given + /// instance. Uses the "2.0.2" tag. + /// + /// The . + /// The name of the resource. + /// The HTTP port. + /// The SMTP port. + /// + /// An instance that + /// represents the added MailDev resource. + /// + public static IResourceBuilder AddMailDev( + this IDistributedApplicationBuilder builder, + string name, + int? httpPort = null, + int? smtpPort = null, + IResourceBuilder? userName = null, + IResourceBuilder? 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 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.0.2"; +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/MailDevResource.AppHost.csproj b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/MailDevResource.AppHost.csproj new file mode 100644 index 0000000000..4e177d9495 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/MailDevResource.AppHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + 9c9bfb14-6706-4421-bc93-37cbaebe36d0 + + + + + + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Program.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Program.cs new file mode 100644 index 0000000000..d291b0c15d --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Program.cs @@ -0,0 +1,14 @@ +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("newsletterservice") + .WithReference(maildev); + +builder.Build().Run(); diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Properties/launchSettings.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..f0583ee1d3 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17251;http://localhost:15199", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21161", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22175" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15199", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19277", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20014" + } + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.Development.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj new file mode 100644 index 0000000000..68f91ed347 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + net8.0 + enable + enable + f2608916-3fcc-4bd8-9697-ce27b4156fc0 + + + diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http new file mode 100644 index 0000000000..39958c6813 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/MailDevResource.NewsletterService.http @@ -0,0 +1,6 @@ +@MailDevResource.NewsletterService_HostAddress = http://localhost:5021 + +GET {{MailDevResource.NewsletterService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Program.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Program.cs new file mode 100644 index 0000000000..45b2365ccd --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Program.cs @@ -0,0 +1,54 @@ +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(); diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Properties/launchSettings.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Properties/launchSettings.json new file mode 100644 index 0000000000..aed106ab5c --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7251;http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.Development.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.NewsletterService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/Extensions.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000000..b6e80000de --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/Extensions.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // builder.Services.AddOpenTelemetry() + // .WithMetrics(metrics => metrics.AddPrometheusExporter()); + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + + // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // app.MapPrometheusScrapingEndpoint(); + } + + return app; + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj new file mode 100644 index 0000000000..b8d847c913 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResource.ServiceDefaults/MailDevResource.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResourceWithCredentials.sln b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResourceWithCredentials.sln new file mode 100644 index 0000000000..88c6bd3078 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailDevResourceWithCredentials.sln @@ -0,0 +1,49 @@ + +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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailDev.Hosting", "MailDev.Hosting\MailDev.Hosting.csproj", "{896B2FA6-E580-4AFC-ACC5-383D052F9EEB}" +EndProject +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 + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70837FCE-AF39-4D07-A9FC-C52ACB32C0AF}.Release|Any CPU.Build.0 = Release|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AE7DA9D-B8AD-4BA6-A358-6F352C2D7255}.Release|Any CPU.Build.0 = Release|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {896B2FA6-E580-4AFC-ACC5-383D052F9EEB}.Release|Any CPU.Build.0 = Release|Any CPU + {3C023F9E-2B5D-4C48-818F-A640EDAE9E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D40F9870-8B8A-4331-BA52-CB368EDD31D6} + EndGlobalSection +EndGlobal diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKit.Client.csproj b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKit.Client.csproj new file mode 100644 index 0000000000..c543c8dbe9 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKit.Client.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientFactory.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientFactory.cs similarity index 94% rename from docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientFactory.cs rename to docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientFactory.cs index 623d3e3e72..f0400a1331 100644 --- a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientFactory.cs +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientFactory.cs @@ -10,9 +10,6 @@ namespace MailKit.Client; /// /// The settings for the SMTP server /// -/// -/// The optional used to authenticate to the SMTP server -/// public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable { private readonly SemaphoreSlim _semaphore = new(1, 1); diff --git a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientSettings.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientSettings.cs similarity index 63% rename from docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientSettings.cs rename to docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientSettings.cs index c75435f179..2e42100ad6 100644 --- a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitClientSettings.cs +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitClientSettings.cs @@ -1,5 +1,5 @@ -using System.Net; -using Microsoft.Extensions.Configuration; +using System.Data.Common; +using System.Net; namespace MailKit.Client; @@ -63,35 +63,41 @@ configuration section. """); } - if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri) is false) + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) { - throw new InvalidOperationException($""" - The 'ConnectionStrings:' (or 'Endpoint' key in - '{DefaultConfigSectionName}') isn't a valid URI format. - """); + Endpoint = uri; } - - Endpoint = uri; - } - - internal void ParseCredentials(IConfigurationSection credentialsSection) - { - if (credentialsSection is null or { Value: null }) + else { - return; - } + var builder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (builder.TryGetValue("Endpoint", out var endpoint) is false) + { + throw new InvalidOperationException($""" + The 'ConnectionStrings:' (or 'Endpoint' key in + '{DefaultConfigSectionName}') is missing. + """); + } - var username = credentialsSection["UserName"]; - var password = credentialsSection["Password"]; + if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var uri) is false) + { + throw new InvalidOperationException($""" + The 'ConnectionStrings:' (or 'Endpoint' key in + '{DefaultConfigSectionName}') isn't a valid URI. + """); + } - if (username is null || password is null) - { - throw new InvalidOperationException($""" - The '{DefaultConfigSectionName}:Credentials' section cannot be empty. - Either remove Credentials altogether, or provide them. - """); + Endpoint = uri; + + if (builder.TryGetValue("Username", out var username) && + builder.TryGetValue("Password", out var password)) + { + Credentials = new( + username.ToString(), password.ToString()); + } } - - Credentials = new(username, password); } } diff --git a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitExtensions.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitExtensions.cs similarity index 96% rename from docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitExtensions.cs rename to docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitExtensions.cs index eb9639b64d..1f9006ffec 100644 --- a/docs/extensibility/snippets/MailDevResource/MailKit.Client/MailKitExtensions.cs +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitExtensions.cs @@ -87,12 +87,6 @@ private static void AddMailKitClient( settings.ParseConnectionString(connectionString); } - if (builder.Configuration.GetSection( - $"{configurationSectionName}:Credentials") is { } section) - { - settings.ParseCredentials(section); - } - configureSettings?.Invoke(settings); if (serviceKey is null) diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitHealthCheck.cs b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitHealthCheck.cs new file mode 100644 index 0000000000..01924a130f --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/MailKit.Client/MailKitHealthCheck.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace MailKit.Client; + +internal sealed class MailKitHealthCheck(MailKitClientFactory factory) : IHealthCheck +{ + public async Task 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); + } + } +} diff --git a/docs/extensibility/snippets/MailDevResourceWithCredentials/global.json b/docs/extensibility/snippets/MailDevResourceWithCredentials/global.json new file mode 100644 index 0000000000..a1aaefd486 --- /dev/null +++ b/docs/extensibility/snippets/MailDevResourceWithCredentials/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.303", + "rollForward": "latestPatch" + } +} diff --git a/docs/toc.yml b/docs/toc.yml index 5b81de742e..6b327f8576 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -33,9 +33,6 @@ items: - name: Persist data using volumes displayName: volumes,persist data href: fundamentals/persist-data-volumes.md - - name: Create custom resource types - displayName: custom resources,extensibility - href: extensibility/custom-resources.md - name: Dashboard items: @@ -64,14 +61,22 @@ items: - name: Telemetry href: fundamentals/telemetry.md + - name: Extensibility + items: + - name: Create custom resource types + displayName: custom resources,extensibility + href: extensibility/custom-resources.md + - name: Create custom components + displayName: custom components,extensibility + href: extensibility/custom-component.md + - name: Implement auth from resource to component + href: extensibility/implement-auth-from-resource-to-component.md + - name: Components items: - name: Overview displayName: aspire components,components,nuget packages,packages href: fundamentals/components-overview.md - - name: Create custom components - displayName: custom components,extensibility - href: extensibility/custom-component.md - name: Tutorials items: - name: Caching using Redis components