// 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; } }