// 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; // 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, callback => { callback.HttpPort = port; }); } /// /// 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 .WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort) .WithHealthCheck(healthCheckKey) .WithWireMockInspectorCommand(); 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); } }); 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. /// 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. /// public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure) { Guard.NotNull(wiremock); wiremock.ApplicationBuilder.Services.TryAddLifecycleHook(); wiremock.Resource.Arguments.ApiMappingBuilder = configure; wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted; 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 . /// public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder builder) { Guard.NotNull(builder); 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 }; builder.WithCommand( name: "wiremock-inspector", displayName: "WireMock Inspector", executeCommand: _ => OnRunOpenInspectorCommandAsync(builder), commandOptions: commandOptions); return builder; } 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; } }