Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions DevProxy.Hosting/DevProxyCommandExtensions.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Adds dashboard commands for controlling Dev Proxy through its API.
/// </summary>
internal static class DevProxyCommandExtensions
{
internal const string ApiTokenEnvironmentVariable = "DEV_PROXY_API_TOKEN";

/// <summary>
/// Generates an API token for the Dev Proxy resource, passes it to Dev Proxy
/// and adds commands to start and stop recording.
/// </summary>
internal static IResourceBuilder<TResource> WithRecordingCommands<TResource>(
this IResourceBuilder<TResource> builder)
where TResource : IResourceWithEndpoints, IResourceWithEnvironment
{
var apiToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));

builder.WithEnvironment(ApiTokenEnvironmentVariable, apiToken);
Comment on lines +27 to +29

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<ExecuteCommandResult> 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; }
}
}
6 changes: 4 additions & 2 deletions DevProxy.Hosting/DevProxyResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static IResourceBuilder<DevProxyExecutableResource> 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;
}
Expand Down Expand Up @@ -77,7 +78,8 @@ public static IResourceBuilder<DevProxyContainerResource> 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;
}
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down