// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.WireMock;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
///
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
///
public static class WireMockServerBuilderExtensions
{
// Linux only (https://github.com/dotnet/aspire/issues/854)
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
///
/// Adds a WireMock.Net Server resource to the application model.
///
/// The .
/// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
/// The HTTP port for the WireMock Server.
/// A reference to the .
public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
return builder.AddWireMock(name, serverArguments =>
{
if (port != null)
{
serverArguments.HttpPorts = [port.Value];
}
});
}
///
/// Adds a WireMock.Net Server resource to the application model.
///
/// The .
/// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
/// The additional urls which the WireMock Server should listen on.
/// A reference to the .
public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, params string[] additionalUrls)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(additionalUrls);
return builder.AddWireMock(name, serverArguments =>
{
serverArguments.WithAdditionalUrls(additionalUrls);
});
}
///
/// Adds a WireMock.Net Server resource to the application model.
///
/// The .
/// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
/// The arguments to start the WireMock.Net Server.
/// A reference to the .
public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
var healthCheckKey = $"{name}_check";
var healthCheckRegistration = new HealthCheckRegistration(
healthCheckKey,
_ => new WireMockHealthCheck(wireMockContainerResource),
failureStatus: null,
tags: null);
builder.Services.AddHealthChecks().Add(healthCheckRegistration);
var resourceBuilder = builder
.AddResource(wireMockContainerResource)
.WithImage(DefaultLinuxImage)
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
.WithHealthCheck(healthCheckKey)
.WithWireMockInspectorCommand();
if (arguments.HttpPorts.Count == 0)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
}
else if (arguments.HttpPorts.Count == 1)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: arguments.HttpPorts[0], targetPort: WireMockServerArguments.HttpContainerPort);
}
else
{
// Required for the default admin endpoint and health checks
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
var anyIsHttp2 = false;
foreach (var url in arguments.AdditionalUrls)
{
PortUtils.TryExtract(url, out _, out var isHttp2, out var scheme, out _, out var httpPort);
anyIsHttp2 |= isHttp2;
resourceBuilder = resourceBuilder.WithEndpoint(port: httpPort, targetPort: httpPort, scheme: scheme, name: $"{scheme}-{httpPort}");
}
if (anyIsHttp2)
{
resourceBuilder = resourceBuilder.AsHttp2Service();
}
}
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
}
resourceBuilder = resourceBuilder.WithArgs(ctx =>
{
foreach (var arg in arguments.GetArgs())
{
ctx.Args.Add(arg);
}
});
// Always add the lifecycle hook to support dynamic mappings and proto definitions
resourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook();
return resourceBuilder;
}
///
/// Adds a WireMock.Net Server resource to the application model.
///
/// The .
/// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
/// A callback that allows for setting the .
/// A reference to the .
public static IResourceBuilder AddWireMock(
this IDistributedApplicationBuilder builder,
string name,
Action callback)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(callback);
var arguments = new WireMockServerArguments();
callback(arguments);
return builder.AddWireMock(name, arguments);
}
///
/// Defines if the static mappings should be read at startup.
///
/// Default set to false.
///
/// A reference to the .
public static IResourceBuilder WithReadStaticMappings(this IResourceBuilder wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
return wiremock;
}
///
/// Watch the static mapping files + folder for changes when running.
///
/// Default set to false.
///
/// A reference to the .
public static IResourceBuilder WithWatchStaticMappings(this IResourceBuilder wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
return wiremock;
}
///
/// Specifies the path for the (static) mapping json files.
///
/// The .
/// The local path.
/// A reference to the .
public static IResourceBuilder WithMappingsPath(this IResourceBuilder wiremock, string mappingsPath)
{
Guard.NotNullOrWhiteSpace(mappingsPath);
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
}
///
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
///
/// The .
/// The admin username.
/// The admin password.
/// A reference to the .
public static IResourceBuilder WithAdminUserNameAndPassword(this IResourceBuilder wiremock, string username, string password)
{
Guard.NotNull(wiremock);
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
return wiremock;
}
///
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
///
/// The .
/// Delegate that will be invoked to configure the WireMock.Net resource.
/// A reference to the .
public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
}
///
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
///
/// The .
/// Delegate that will be invoked to configure the WireMock.Net resource.
/// A reference to the .
public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure)
{
Guard.NotNull(wiremock);
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;
return wiremock;
}
///
/// Add a Grpc ProtoDefinition at server-level.
///
/// The .
/// Unique identifier for the ProtoDefinition.
/// The ProtoDefinition as text.
/// A reference to the .
public static IResourceBuilder WithProtoDefinition(this IResourceBuilder wiremock, string id, params string[] protoDefinitions)
{
Guard.NotNull(wiremock).Resource.Arguments.WithProtoDefinition(id, protoDefinitions);
return wiremock;
}
///
/// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting.
/// This requires installation of the WireMockInspector tool.
///
/// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
///
///
/// The .
/// A reference to the .
public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder wiremock)
{
Guard.NotNull(wiremock);
CommandOptions commandOptions = new()
{
Description = "Requires installation of the WireMockInspector (https://github.com/WireMock-Net/WireMockInspector) tool:\ndotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources",
UpdateState = OnUpdateResourceState,
IconName = "BoxSearch",
IconVariant = IconVariant.Filled
};
wiremock.WithCommand(
name: "wiremock-inspector",
displayName: "WireMock Inspector",
executeCommand: _ => OnRunOpenInspectorCommandAsync(wiremock),
commandOptions: commandOptions);
return wiremock;
}
///
/// Configures OpenTelemetry distributed tracing for the WireMock.Net server.
/// This enables automatic trace export to the Aspire dashboard.
///
/// The .
/// A reference to the .
///
/// When enabled, WireMock.Net will emit distributed traces for each request processed,
/// including information about:
///
/// - HTTP method, URL, and status code
/// - Mapping match results and scores
/// - Request processing duration
///
/// The traces will automatically appear in the Aspire dashboard.
///
public static IResourceBuilder WithOpenTelemetry(this IResourceBuilder wiremock)
{
Guard.NotNull(wiremock);
// Enable OpenTelemetry in WireMock server arguments
wiremock.Resource.Arguments.OpenTelemetryEnabled = true;
// Use Aspire's standard WithOtlpExporter to configure OTEL environment variables for the container
// This sets OTEL_EXPORTER_OTLP_ENDPOINT which the OTLP exporter reads automatically
var containerBuilder = wiremock as IResourceBuilder;
if (containerBuilder != null)
{
containerBuilder.WithOtlpExporter();
}
return wiremock;
}
private static Task OnRunOpenInspectorCommandAsync(IResourceBuilder builder)
{
WireMockInspector.Inspect(builder.Resource.GetEndpoint().Url);
return Task.FromResult(CommandResults.Success());
}
private static ResourceCommandState OnUpdateResourceState(UpdateCommandStateContext context)
{
return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
}
}