-
Notifications
You must be signed in to change notification settings - Fork 153
Add extensibility article on how to write non-container custom resources #3850
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
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ae7651e
Initial plan for issue
Copilot 944bda3
Add non-container custom resource article with lifecycle hooks example
Copilot 6ea8f13
Add cross-references and improve navigation between extensibility art…
Copilot 2fba711
Address PR feedback: Add missing project files, update versions, impr…
Copilot 192c993
Fix .slnx syntax and move custom resources to Dev-time orchestration …
Copilot 18f672b
Fix bullet points, .slnx syntax, and TOC formatting based on PR feedback
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
--- | ||
title: Create non-container custom resources | ||
description: Learn how to create custom .NET Aspire resources that don't rely on containers using lifecycle hooks and dashboard integration. | ||
ms.date: 06/25/2025 | ||
ms.topic: how-to | ||
--- | ||
|
||
# Create non-container custom resources | ||
|
||
While many .NET Aspire resources are container-based, you can also create custom resources that run in-process or manage external services without containers. This article shows how to build a non-container custom resource that integrates with the Aspire dashboard using lifecycle hooks, status notifications, and logging. | ||
|
||
## When to use non-container resources | ||
|
||
Before creating a custom resource, consider whether your scenario might be better served by simpler approaches: | ||
|
||
- **Connection strings only**: If you just need to connect to an external service, <xref:Aspire.Hosting.ParameterResourceBuilderExtensions.AddConnectionString*> might suffice. | ||
- **Configuration values**: For simple configuration, <xref:Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter*> might be enough. | ||
|
||
Custom non-container resources are valuable when you need: | ||
|
||
- Dashboard integration with status updates and logs. | ||
- Lifecycle management (starting/stopping services). | ||
- In-process services that benefit from Aspire's orchestration. | ||
- External resource management with rich feedback. | ||
|
||
Examples include: | ||
|
||
- In-process HTTP proxies or middleware. | ||
- Cloud service provisioning and management. | ||
- External API integrations with health monitoring. | ||
- Background services that need dashboard visibility. | ||
|
||
## Key components | ||
|
||
Non-container custom resources use these key Aspire services: | ||
|
||
- **<xref:Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook>**: Hook into app startup/shutdown. For more information, see [App Host life cycle events](../app-host/eventing.md#app-host-life-cycle-events). | ||
- **<xref:Microsoft.Extensions.Logging.ILogger>**: Standard .NET logging that appears in console and dashboard | ||
|
||
> [!NOTE] | ||
> Advanced dashboard integration is possible using services like `ResourceNotificationService` and `ResourceLoggerService` for real-time status updates and log streaming. These APIs provide richer dashboard experiences but require more complex implementation. | ||
|
||
## Example: HTTP proxy resource | ||
|
||
This example creates an in-process HTTP proxy resource that demonstrates the core concepts of lifecycle management and logging integration with the Aspire dashboard. | ||
|
||
### Define the resource | ||
|
||
First, create the resource class: | ||
|
||
:::code language="csharp" source="snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs"::: | ||
|
||
The resource implements <xref:Aspire.Hosting.ApplicationModel.IResource> and includes properties for the proxy configuration. | ||
|
||
### Create the extension method | ||
|
||
Next, create the builder extension: | ||
|
||
:::code language="csharp" source="snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs"::: | ||
|
||
This extension method adds the resource to the application model and configures an HTTP endpoint. | ||
|
||
### Implement lifecycle management | ||
|
||
Create a lifecycle hook to manage the proxy: | ||
|
||
:::code language="csharp" source="snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs"::: | ||
|
||
The lifecycle hook: | ||
|
||
1. **Manages lifecycle**: Starts services when resources are created. | ||
1. **Integrates logging**: Uses standard .NET logging that appears in the Aspire dashboard. | ||
1. **Handles background tasks**: Runs long-running services in background tasks. | ||
1. **Provides resource management**: Manages resources like HTTP listeners and cleanup. | ||
|
||
### Register the lifecycle hook | ||
|
||
The extension method automatically registers the lifecycle hook: | ||
|
||
:::code language="csharp" source="snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs"::: | ||
|
||
### Use the resource | ||
|
||
Now you can use the proxy in your app host: | ||
|
||
:::code language="csharp" source="snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs"::: | ||
|
||
## Dashboard integration | ||
|
||
Non-container resources integrate with the Aspire dashboard through multiple channels, providing real-time visibility into your resource's status, logs, and performance. | ||
|
||
### Standard logging | ||
|
||
All .NET logging automatically appears in the Aspire dashboard. Use structured logging for better searchability and filtering: | ||
|
||
```csharp | ||
_logger.LogInformation("Starting HTTP proxy {ResourceName} -> {TargetUrl}", | ||
resource.Name, resource.TargetUrl); | ||
_logger.LogError(ex, "Failed to start HTTP proxy {ResourceName}", resource.Name); | ||
``` | ||
|
||
### Advanced dashboard features | ||
|
||
For more sophisticated dashboard integration, you can use: | ||
|
||
- **Status notifications**: Update resource state and properties in real-time using advanced Aspire hosting APIs | ||
- **Log streaming**: Send structured logs directly to the dashboard using specialized services | ||
- **Health monitoring**: Report resource health and performance metrics. For more information, see [Health checks in .NET Aspire](../fundamentals/health-checks.md) | ||
|
||
For more information about the dashboard's capabilities, see [Explore the .NET Aspire dashboard](../fundamentals/dashboard/explore.md). | ||
|
||
These advanced features require additional Aspire hosting APIs and more complex implementation patterns. | ||
|
||
## Best practices | ||
|
||
When creating non-container resources: | ||
|
||
1. **Resource cleanup**: Always implement proper disposal in lifecycle hooks | ||
1. **Error handling**: Catch and log exceptions, update status appropriately | ||
1. **Status updates**: Provide meaningful status information to users | ||
1. **Performance**: Avoid blocking operations in lifecycle methods | ||
1. **Dependencies**: Use dependency injection for required services | ||
|
||
## Summary | ||
|
||
Non-container custom resources extend .NET Aspire beyond containers to include in-process services and external resource management. By implementing lifecycle hooks and integrating with the dashboard through status notifications and logging, you can create rich development experiences for any type of resource your application needs. | ||
|
||
## Next steps | ||
|
||
> [!div class="nextstepaction"] | ||
> [Create custom .NET Aspire client integrations](custom-client-integration.md) |
13 changes: 13 additions & 0 deletions
13
docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxy.Hosting.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net9.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Aspire.Hosting" Version="9.3.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
102 changes: 102 additions & 0 deletions
102
docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
using System.Net; | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Lifecycle; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace HttpProxy.Hosting; | ||
|
||
/// <summary> | ||
/// Lifecycle hook that manages HTTP proxy resources. | ||
/// </summary> | ||
public class HttpProxyLifecycleHook : IDistributedApplicationLifecycleHook | ||
{ | ||
private readonly ILogger<HttpProxyLifecycleHook> _logger; | ||
private readonly Dictionary<string, HttpListener> _listeners = new(); | ||
|
||
public HttpProxyLifecycleHook(ILogger<HttpProxyLifecycleHook> logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) | ||
{ | ||
// Find and start HTTP proxy resources | ||
var proxyResources = appModel.Resources.OfType<HttpProxyResource>(); | ||
|
||
foreach (var resource in proxyResources) | ||
{ | ||
StartProxy(resource); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private void StartProxy(HttpProxyResource resource) | ||
{ | ||
try | ||
{ | ||
_logger.LogInformation("Starting HTTP proxy {ResourceName} -> {TargetUrl}", | ||
resource.Name, resource.TargetUrl); | ||
|
||
// Create and start HTTP listener on a dynamic port | ||
var listener = new HttpListener(); | ||
listener.Prefixes.Add("http://localhost:0/"); // Use system-assigned port | ||
listener.Start(); | ||
|
||
_listeners[resource.Name] = listener; | ||
|
||
// Start processing requests in the background | ||
_ = Task.Run(() => ProcessRequests(resource, listener)); | ||
|
||
_logger.LogInformation("HTTP proxy {ResourceName} started successfully", resource.Name); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Failed to start HTTP proxy {ResourceName}", resource.Name); | ||
} | ||
} | ||
|
||
private async Task ProcessRequests(HttpProxyResource resource, HttpListener listener) | ||
{ | ||
var requestCount = 0; | ||
|
||
while (listener.IsListening) | ||
{ | ||
try | ||
{ | ||
var context = await listener.GetContextAsync(); | ||
requestCount++; | ||
|
||
_logger.LogInformation("Proxy {ResourceName} handling request {RequestCount}: {Method} {Path}", | ||
resource.Name, requestCount, context.Request.HttpMethod, context.Request.Url?.PathAndQuery); | ||
|
||
// Simple response for demonstration | ||
var response = context.Response; | ||
response.StatusCode = 200; | ||
var responseString = $"Proxy {resource.Name} would forward to {resource.TargetUrl}"; | ||
var buffer = System.Text.Encoding.UTF8.GetBytes(responseString); | ||
await response.OutputStream.WriteAsync(buffer); | ||
response.Close(); | ||
} | ||
catch (HttpListenerException) | ||
{ | ||
// Listener was stopped | ||
break; | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Error processing request in proxy {ResourceName}", resource.Name); | ||
} | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using Aspire.Hosting.ApplicationModel; | ||
|
||
namespace HttpProxy.Hosting; | ||
|
||
/// <summary> | ||
/// Represents an HTTP proxy resource that forwards requests to a target URL. | ||
/// </summary> | ||
/// <param name="name">The name of the resource.</param> | ||
/// <param name="targetUrl">The target URL to proxy requests to.</param> | ||
public class HttpProxyResource(string name, string targetUrl) : Resource(name), IResourceWithEndpoints | ||
{ | ||
/// <summary> | ||
/// Gets the target URL that requests will be proxied to. | ||
/// </summary> | ||
public string TargetUrl { get; } = targetUrl; | ||
|
||
/// <summary> | ||
/// Gets the name of the HTTP endpoint. | ||
/// </summary> | ||
public const string HttpEndpointName = "http"; | ||
} |
36 changes: 36 additions & 0 deletions
36
...bility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Lifecycle; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.DependencyInjection.Extensions; | ||
|
||
namespace Aspire.Hosting; | ||
|
||
/// <summary> | ||
/// Extension methods for adding HTTP proxy resources to the application model. | ||
/// </summary> | ||
public static class HttpProxyResourceBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds an HTTP proxy resource to the application model. | ||
/// </summary> | ||
/// <param name="builder">The distributed application builder.</param> | ||
/// <param name="name">The name of the resource.</param> | ||
/// <param name="targetUrl">The target URL to proxy requests to.</param> | ||
/// <param name="port">The port to listen on (optional).</param> | ||
/// <returns>A resource builder for the HTTP proxy resource.</returns> | ||
public static IResourceBuilder<HttpProxy.Hosting.HttpProxyResource> AddHttpProxy( | ||
this IDistributedApplicationBuilder builder, | ||
string name, | ||
string targetUrl, | ||
int? port = null) | ||
{ | ||
var resource = new HttpProxy.Hosting.HttpProxyResource(name, targetUrl); | ||
|
||
// Register the lifecycle hook for this resource type | ||
builder.Services.TryAddSingleton<HttpProxy.Hosting.HttpProxyLifecycleHook>(); | ||
builder.Services.AddLifecycleHook<HttpProxy.Hosting.HttpProxyLifecycleHook>(); | ||
|
||
return builder.AddResource(resource) | ||
.WithHttpEndpoint(port: port, name: HttpProxy.Hosting.HttpProxyResource.HttpEndpointName); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...ibility/snippets/HttpProxyResource/HttpProxySample.AppHost/HttpProxySample.AppHost.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" /> | ||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net9.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<UserSecretsId>HttpProxySample-AppHost-b8d82d85-6a93-4b67-9a70-77fe8d9e6c5c</UserSecretsId> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.3.1" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<ProjectReference Include="..\HttpProxy.Hosting\HttpProxy.Hosting.csproj" IsAspireProjectResource="false" /> | ||
</ItemGroup> | ||
</Project> |
12 changes: 12 additions & 0 deletions
12
docs/extensibility/snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using Aspire.Hosting; | ||
|
||
var builder = DistributedApplication.CreateBuilder(args); | ||
|
||
// Add an HTTP proxy that forwards requests to an external API | ||
var proxy = builder.AddHttpProxy("api-proxy", "https://jsonplaceholder.typicode.com", port: 5100); | ||
|
||
// Add a web project that can use the proxy | ||
var webapp = builder.AddProject<Projects.WebApp>("webapp") | ||
.WithReference(proxy); | ||
|
||
builder.Build().Run(); |
7 changes: 7 additions & 0 deletions
7
docs/extensibility/snippets/HttpProxyResource/HttpProxySample.slnx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<Project> | ||
<Sdk Name="Microsoft.Build.Traversal" /> | ||
<folder name="/src/"> | ||
<project path="HttpProxy.Hosting/HttpProxy.Hosting.csproj" /> | ||
<project path="HttpProxySample.AppHost/HttpProxySample.AppHost.csproj" /> | ||
</folder> | ||
</Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is invalid
.slnx
syntax. If you're not familiar with it, here's an example from one of my personal projects to learn from:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the .slnx file syntax by converting from invalid JSON format to proper XML format with ProjectReference elements. (192c993)