From ae7651e927320779abb865f4c82e61d465894ac9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:35:42 +0000 Subject: [PATCH 1/3] Initial plan for issue From 944bda3ff4fe3dc4a61df41c807b7f50a9eaf311 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:45:40 +0000 Subject: [PATCH 2/3] Add non-container custom resource article with lifecycle hooks example Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> --- .../custom-non-container-resource.md | 145 ++++++++++++++++++ .../HttpProxy.Hosting.csproj | 13 ++ .../HttpProxyLifecycleHook.cs | 102 ++++++++++++ .../HttpProxy.Hosting/HttpProxyResource.cs | 21 +++ .../HttpProxyResourceBuilderExtensions.cs | 36 +++++ .../HttpProxySample.AppHost/Program.cs | 12 ++ docs/toc.yml | 3 + 7 files changed, 332 insertions(+) create mode 100644 docs/extensibility/custom-non-container-resource.md create mode 100644 docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxy.Hosting.csproj create mode 100644 docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs create mode 100644 docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs create mode 100644 docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs create mode 100644 docs/extensibility/snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs diff --git a/docs/extensibility/custom-non-container-resource.md b/docs/extensibility/custom-non-container-resource.md new file mode 100644 index 0000000000..6cb3e6377d --- /dev/null +++ b/docs/extensibility/custom-non-container-resource.md @@ -0,0 +1,145 @@ +--- +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/18/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, might suffice. +- **Configuration values**: For simple configuration, 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: + +- ****: Hook into app startup/shutdown +- ****: 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 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 +2. **Integrates logging**: Uses standard .NET logging that appears in the Aspire dashboard +3. **Handles background tasks**: Runs long-running services in background tasks +4. **Provides resource management**: Manages resources like HTTP listeners and cleanup + +### Register the lifecycle hook + +The extension method automatically registers the lifecycle hook: + +```csharp +public static IResourceBuilder AddHttpProxy( + this IDistributedApplicationBuilder builder, + string name, + string targetUrl, + int? port = null) +{ + var resource = new HttpProxyResource(name, targetUrl); + + // Register the lifecycle hook for this resource type + builder.Services.TryAddSingleton(); + builder.Services.AddLifecycleHook(); + + return builder.AddResource(resource) + .WithHttpEndpoint(port: port, name: "http"); +} +``` + +### 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 + +The resource integrates with the Aspire dashboard through: + +### Standard logging + +Use standard .NET logging patterns that automatically appear in the dashboard: + +```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 +- **Log streaming**: Send structured logs directly to the dashboard +- **Health monitoring**: Report resource health and performance metrics + +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 +2. **Error handling**: Catch and log exceptions, update status appropriately +3. **Status updates**: Provide meaningful status information to users +4. **Performance**: Avoid blocking operations in lifecycle methods +5. **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) diff --git a/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxy.Hosting.csproj b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxy.Hosting.csproj new file mode 100644 index 0000000000..77a49c1233 --- /dev/null +++ b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxy.Hosting.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + \ No newline at end of file diff --git a/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs new file mode 100644 index 0000000000..8947c5241c --- /dev/null +++ b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyLifecycleHook.cs @@ -0,0 +1,102 @@ +using System.Net; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; +using Microsoft.Extensions.Logging; + +namespace HttpProxy.Hosting; + +/// +/// Lifecycle hook that manages HTTP proxy resources. +/// +public class HttpProxyLifecycleHook : IDistributedApplicationLifecycleHook +{ + private readonly ILogger _logger; + private readonly Dictionary _listeners = new(); + + public HttpProxyLifecycleHook(ILogger 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(); + + 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); + } + } + } +} \ No newline at end of file diff --git a/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs new file mode 100644 index 0000000000..88e2d1ecb8 --- /dev/null +++ b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResource.cs @@ -0,0 +1,21 @@ +using Aspire.Hosting.ApplicationModel; + +namespace HttpProxy.Hosting; + +/// +/// Represents an HTTP proxy resource that forwards requests to a target URL. +/// +/// The name of the resource. +/// The target URL to proxy requests to. +public class HttpProxyResource(string name, string targetUrl) : Resource(name), IResourceWithEndpoints +{ + /// + /// Gets the target URL that requests will be proxied to. + /// + public string TargetUrl { get; } = targetUrl; + + /// + /// Gets the name of the HTTP endpoint. + /// + public const string HttpEndpointName = "http"; +} \ No newline at end of file diff --git a/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs new file mode 100644 index 0000000000..7f67b6f263 --- /dev/null +++ b/docs/extensibility/snippets/HttpProxyResource/HttpProxy.Hosting/HttpProxyResourceBuilderExtensions.cs @@ -0,0 +1,36 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Aspire.Hosting; + +/// +/// Extension methods for adding HTTP proxy resources to the application model. +/// +public static class HttpProxyResourceBuilderExtensions +{ + /// + /// Adds an HTTP proxy resource to the application model. + /// + /// The distributed application builder. + /// The name of the resource. + /// The target URL to proxy requests to. + /// The port to listen on (optional). + /// A resource builder for the HTTP proxy resource. + public static IResourceBuilder 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(); + builder.Services.AddLifecycleHook(); + + return builder.AddResource(resource) + .WithHttpEndpoint(port: port, name: HttpProxy.Hosting.HttpProxyResource.HttpEndpointName); + } +} \ No newline at end of file diff --git a/docs/extensibility/snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs b/docs/extensibility/snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs new file mode 100644 index 0000000000..78de14ef71 --- /dev/null +++ b/docs/extensibility/snippets/HttpProxyResource/HttpProxySample.AppHost/Program.cs @@ -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("webapp") + .WithReference(proxy); + +builder.Build().Run(); \ No newline at end of file diff --git a/docs/toc.yml b/docs/toc.yml index 4e4bdefe24..223bee4adc 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -378,6 +378,9 @@ items: - name: Create hosting integrations displayName: resources,extensibility,hosting,integrations href: extensibility/custom-hosting-integration.md + - name: Create non-container custom resources + displayName: resources,extensibility,hosting,integrations,lifecycle,dashboard + href: extensibility/custom-non-container-resource.md - name: Create client integrations displayName: custom,extensibility,client,library,integrations href: extensibility/custom-client-integration.md From 6ea8f133066ee3e638fd9f2a90621ae3f2ddf6fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:46:41 +0000 Subject: [PATCH 3/3] Add cross-references and improve navigation between extensibility articles Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> --- docs/extensibility/custom-hosting-integration.md | 3 +++ docs/extensibility/custom-non-container-resource.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/extensibility/custom-hosting-integration.md b/docs/extensibility/custom-hosting-integration.md index 60ffe37daa..5a86678ca1 100644 --- a/docs/extensibility/custom-hosting-integration.md +++ b/docs/extensibility/custom-hosting-integration.md @@ -487,5 +487,8 @@ In the custom resource tutorial, you learned how to create a custom .NET Aspire ## Next steps +> [!div class="nextstepaction"] +> [Create non-container custom resources](custom-non-container-resource.md) + > [!div class="nextstepaction"] > [Create custom .NET Aspire client integrations](custom-client-integration.md) diff --git a/docs/extensibility/custom-non-container-resource.md b/docs/extensibility/custom-non-container-resource.md index 6cb3e6377d..3e3bcd19cd 100644 --- a/docs/extensibility/custom-non-container-resource.md +++ b/docs/extensibility/custom-non-container-resource.md @@ -34,7 +34,7 @@ Examples include: Non-container custom resources use these key Aspire services: -- ****: Hook into app startup/shutdown +- ****: Hook into app startup/shutdown. For more information, see [App Host life cycle events](../app-host/eventing.md#app-host-life-cycle-events). - ****: Standard .NET logging that appears in console and dashboard > [!NOTE]