diff --git a/DevProxy.Hosting/DevProxyCommandExtensions.cs b/DevProxy.Hosting/DevProxyCommandExtensions.cs
new file mode 100644
index 0000000..b9f2b12
--- /dev/null
+++ b/DevProxy.Hosting/DevProxyCommandExtensions.cs
@@ -0,0 +1,119 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Security.Cryptography;
+using Aspire.Hosting.ApplicationModel;
+using DevProxy.Hosting;
+
+#pragma warning disable IDE0130
+namespace Aspire.Hosting;
+#pragma warning restore IDE0130
+
+///
+/// Adds dashboard commands for controlling Dev Proxy through its API.
+///
+internal static class DevProxyCommandExtensions
+{
+ internal const string ApiTokenEnvironmentVariable = "DEV_PROXY_API_TOKEN";
+
+ ///
+ /// Generates an API token for the Dev Proxy resource, passes it to Dev Proxy
+ /// and adds commands to start and stop recording.
+ ///
+ internal static IResourceBuilder WithRecordingCommands(
+ this IResourceBuilder builder)
+ where TResource : IResourceWithEndpoints, IResourceWithEnvironment
+ {
+ var apiToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
+
+ builder.WithEnvironment(ApiTokenEnvironmentVariable, apiToken);
+
+ builder.WithCommand(
+ name: "start-recording",
+ displayName: "Start recording",
+ executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: true, context),
+ commandOptions: new CommandOptions
+ {
+ Description = "Start recording requests in Dev Proxy",
+ IconName = "Record",
+ UpdateState = UpdateCommandState
+ });
+
+ builder.WithCommand(
+ name: "stop-recording",
+ displayName: "Stop recording",
+ executeCommand: context => SetRecordingAsync(builder.Resource, apiToken, recording: false, context),
+ commandOptions: new CommandOptions
+ {
+ Description = "Stop recording requests in Dev Proxy",
+ IconName = "RecordStop",
+ UpdateState = UpdateCommandState
+ });
+
+ return builder;
+ }
+
+ private static ResourceCommandState UpdateCommandState(UpdateCommandStateContext context) =>
+ context.ResourceSnapshot.State?.Text == KnownResourceStates.Running
+ ? ResourceCommandState.Enabled
+ : ResourceCommandState.Disabled;
+
+ private static async Task SetRecordingAsync(
+ IResourceWithEndpoints resource,
+ string apiToken,
+ bool recording,
+ ExecuteCommandContext context)
+ {
+ try
+ {
+ var endpointUrl = await resource
+ .GetEndpoint(DevProxyResource.ApiEndpointName)
+ .GetValueAsync(context.CancellationToken);
+
+ if (string.IsNullOrWhiteSpace(endpointUrl))
+ {
+ return CommandResults.Failure("The Dev Proxy API endpoint is not available.");
+ }
+
+ using var httpClient = new HttpClient(new HttpClientHandler { UseProxy = false })
+ {
+ BaseAddress = new Uri(endpointUrl),
+ Timeout = TimeSpan.FromSeconds(10)
+ };
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
+
+ using var response = await httpClient.PostAsJsonAsync(
+ "/proxy",
+ new DevProxyUpdate { Recording = recording },
+ context.CancellationToken);
+
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ return CommandResults.Failure(
+ $"Dev Proxy rejected the API token. Make sure you're using a version of Dev Proxy that supports the {ApiTokenEnvironmentVariable} environment variable.");
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var content = await response.Content.ReadAsStringAsync(context.CancellationToken);
+ return CommandResults.Failure(
+ $"Dev Proxy returned {(int)response.StatusCode} {response.ReasonPhrase}: {content}");
+ }
+
+ return CommandResults.Success(recording ? "Recording started." : "Recording stopped.");
+ }
+ catch (HttpRequestException exception)
+ {
+ return CommandResults.Failure($"Could not reach the Dev Proxy API: {exception.Message}");
+ }
+ catch (TaskCanceledException) when (!context.CancellationToken.IsCancellationRequested)
+ {
+ return CommandResults.Failure("The Dev Proxy API did not respond in time.");
+ }
+ }
+
+ private sealed class DevProxyUpdate
+ {
+ public bool? Recording { get; set; }
+ }
+}
diff --git a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs
index e81b574..bcdb15c 100644
--- a/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs
+++ b/DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs
@@ -25,7 +25,8 @@ public static IResourceBuilder AddDevProxyExecutable
var resourceBuilder = builder.AddResource(resource)
.WithArgs("--as-system-proxy", "false")
.WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName)
- .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName);
+ .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName)
+ .WithRecordingCommands();
return resourceBuilder;
}
@@ -77,7 +78,8 @@ public static IResourceBuilder AddDevProxyContainer(
.WithImageRegistry(DevProxyContainerImageTags.Registry)
.WithImageTag(DevProxyContainerImageTags.Tag)
.WithHttpEndpoint(targetPort: 8000, name: DevProxyResource.ProxyEndpointName)
- .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName);
+ .WithHttpEndpoint(targetPort: 8897, name: DevProxyResource.ApiEndpointName)
+ .WithRecordingCommands();
return resourceBuilder;
}
diff --git a/README.md b/README.md
index ea1d596..1b8a434 100644
--- a/README.md
+++ b/README.md
@@ -91,9 +91,12 @@ builder.Build().Run();
- **Container Resource**: Use `AddDevProxyContainer` to run Dev Proxy as a containerized service.
- **Executable Resource**: Use `AddDevProxyExecutable` to run Dev Proxy from the locally installed executable.
+- **Recording Commands**: Start and stop Dev Proxy recording from the Aspire dashboard for either resource type.
- **Custom Configuration**: Use the extension methods to configure Dev Proxy as needed.
- **Service Integration**: Easily integrate Dev Proxy with other services in your application.
+Both Dev Proxy resource types expose `Start recording` and `Stop recording` commands in the Aspire dashboard. The commands require a version of Dev Proxy that supports the `DEV_PROXY_API_TOKEN` environment variable.
+
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.