From 30fa960f7b9212fba4e72c7781f7b5ed52c6613a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 23 Nov 2025 10:26:12 +0100 Subject: [PATCH 1/9] Add property UseHttp2 to WireMockServerArguments --- src/WireMock.Net.Aspire/WireMockMappingState.cs | 10 ++++++---- src/WireMock.Net.Aspire/WireMockServerArguments.cs | 10 ++++++++++ src/WireMock.Net.Aspire/WireMockServerResource.cs | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/WireMock.Net.Aspire/WireMockMappingState.cs b/src/WireMock.Net.Aspire/WireMockMappingState.cs index 2bb82108a..a8e6c01b7 100644 --- a/src/WireMock.Net.Aspire/WireMockMappingState.cs +++ b/src/WireMock.Net.Aspire/WireMockMappingState.cs @@ -4,7 +4,9 @@ namespace WireMock.Net.Aspire; internal enum WireMockMappingState { - NoMappings, - NotSubmitted, - Submitted, -} + NoMappings = 0, + + NotSubmitted = 1, + + Submitted = 2 +} \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index 8c059c1a1..9ac20774b 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -67,6 +67,11 @@ public class WireMockServerArguments /// public Func? ApiMappingBuilder { get; set; } + /// + /// Use HTTP 2 (used for Grpc). + /// + public bool UseHttp2 { get; set; } + /// /// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server. /// @@ -95,6 +100,11 @@ public string[] GetArgs() Add(args, "--WatchStaticMappingsInSubdirectories", "true"); } + if (UseHttp2) + { + Add(args, "--UseHttp2", "true"); + } + return args .SelectMany(k => new[] { k.Key, k.Value }) .ToArray(); diff --git a/src/WireMock.Net.Aspire/WireMockServerResource.cs b/src/WireMock.Net.Aspire/WireMockServerResource.cs index 528b21003..59e187993 100644 --- a/src/WireMock.Net.Aspire/WireMockServerResource.cs +++ b/src/WireMock.Net.Aspire/WireMockServerResource.cs @@ -113,7 +113,7 @@ private IWireMockAdminApi CreateWireMockAdminApi() private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args) { - _logger?.LogInformation("MappingFile created, changed or deleted: '{0}'. Triggering ReloadStaticMappings.", args.FullPath); + _logger?.LogInformation("MappingFile created, changed or deleted: '{FullPath}'. Triggering ReloadStaticMappings.", args.FullPath); try { await AdminApi.Value.ReloadStaticMappingsAsync(); From 36995353c77b35a828a517f8e7b3295007ff109a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 24 Nov 2025 17:51:16 +0100 Subject: [PATCH 2/9] . --- .../WireMock.Net.Aspire.csproj | 5 ++- .../WireMockServerArguments.cs | 19 ++++++++- .../WireMockServerBuilderExtensions.cs | 42 +++++++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj index a835e090f..088098d4f 100644 --- a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj +++ b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj @@ -30,7 +30,10 @@ - + + + + diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index 9ac20774b..bed5d1ae7 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net using System.Diagnostics.CodeAnalysis; +using Stef.Validation; using WireMock.Client.Builders; // ReSharper disable once CheckNamespace @@ -24,7 +25,7 @@ public class WireMockServerArguments /// The HTTP port where WireMock.Net is listening. /// If not defined, .NET Aspire automatically assigns a random port. /// - public int? HttpPort { get; set; } + public List HttpPorts { get; set; } = []; /// /// The admin username. @@ -72,6 +73,22 @@ public class WireMockServerArguments /// public bool UseHttp2 { get; set; } + /// + /// Aadditional Urls on which WireMock listens. + /// + public List Urls { get; set; } = []; + + /// + /// Add an additional Url on which WireMock listens. + /// + /// The url to add. + /// The port to add. + internal void WithAdditionalUrlWithPort(string url, int port) + { + Urls.Add(Guard.NotNullOrWhiteSpace(url)); + HttpPorts.Add(port); + } + /// /// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server. /// diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index b19e192e2..55e4b8638 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -8,6 +8,7 @@ using Stef.Validation; using WireMock.Client.Builders; using WireMock.Net.Aspire; +using WireMock.Util; // ReSharper disable once CheckNamespace namespace Aspire.Hosting; @@ -36,7 +37,10 @@ public static IResourceBuilder AddWireMock(this IDistrib return builder.AddWireMock(name, callback => { - callback.HttpPort = port; + if (port != null) + { + callback.HttpPorts = [port.Value]; + } }); } @@ -67,10 +71,21 @@ public static IResourceBuilder AddWireMock(this IDistrib .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 (arguments.HttpPorts.Count == 0) + { + resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort); + } + else + { + foreach (var httpPort in arguments.HttpPorts) + { + resourceBuilder = resourceBuilder.WithHttpEndpoint(port: httpPort, targetPort: WireMockServerArguments.HttpContainerPort); + } + } + if (!string.IsNullOrEmpty(arguments.MappingsPath)) { resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath); @@ -165,7 +180,7 @@ public static IResourceBuilder WithAdminUserNameAndPassw /// /// 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)); @@ -176,7 +191,7 @@ public static IResourceBuilder WithApiMappingBuilder(thi /// /// 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); @@ -188,6 +203,25 @@ public static IResourceBuilder WithApiMappingBuilder(thi return wiremock; } + /// + /// Adds another URL to the WireMock container. By default, the WireMock container will listen on http://*:80. + /// + /// This method can be used to also host the WireMock container on another port or protocol (like grpc). + /// + /// grpc://*:9090 + /// A reference to the . + public static IResourceBuilder AddUrl(this IResourceBuilder wiremock, string url) + { + if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port)) + { + throw new ArgumentException("The URL is not valid.", nameof(url)); + } + + wiremock.Resource.Arguments.WithAdditionalUrlWithPort(url, port); + + return wiremock; + } + /// /// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting. /// This requires installation of the WireMockInspector tool. From 244b015d8ffaf74b94ed0e30ba70c53dfb7807c0 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 24 Nov 2025 18:26:14 +0100 Subject: [PATCH 3/9] additionalUrls --- examples-Aspire/AspireApp1.AppHost/Program.cs | 5 +- .../WireMockServerArguments.cs | 9 ++- .../WireMockServerBuilderExtensions.cs | 67 ++++++++++--------- .../Settings/SimpleSettingsParser.cs | 2 +- .../Settings/WireMockServerSettingsParser.cs | 2 +- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index 451cf92ef..cfd42e126 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -7,7 +7,8 @@ var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings"); IResourceBuilder apiService = builder - .AddWireMock("apiservice", WireMockServerArguments.DefaultPort) + //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort) + .AddWireMock("apiservice", ["http://*:8080", "grpc://*:9090"]) .WithMappingsPath(mappingsPath) .WithReadStaticMappings() .WithWatchStaticMappings() @@ -48,4 +49,4 @@ .WithReference(apiService) .WaitFor(apiService); -builder.Build().Run(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index bed5d1ae7..06702409e 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -76,7 +76,7 @@ public class WireMockServerArguments /// /// Aadditional Urls on which WireMock listens. /// - public List Urls { get; set; } = []; + public List AdditionalUrls { get; set; } = []; /// /// Add an additional Url on which WireMock listens. @@ -85,7 +85,7 @@ public class WireMockServerArguments /// The port to add. internal void WithAdditionalUrlWithPort(string url, int port) { - Urls.Add(Guard.NotNullOrWhiteSpace(url)); + AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url)); HttpPorts.Add(port); } @@ -122,6 +122,11 @@ public string[] GetArgs() Add(args, "--UseHttp2", "true"); } + if (AdditionalUrls.Count > 0) + { + Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}"); + } + return args .SelectMany(k => new[] { k.Key, k.Value }) .ToArray(); diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index 55e4b8638..62a7a9ff3 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -35,11 +35,38 @@ public static IResourceBuilder AddWireMock(this IDistrib Guard.NotNullOrWhiteSpace(name); Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue); - return builder.AddWireMock(name, callback => + return builder.AddWireMock(name, serverArguments => { if (port != null) { - callback.HttpPorts = [port.Value]; + 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 listens 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 => + { + foreach (var url in additionalUrls) + { + if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port)) + { + throw new ArgumentException($"The URL {url} is not valid."); + } + + serverArguments.WithAdditionalUrlWithPort(url, port); } }); } @@ -70,20 +97,14 @@ public static IResourceBuilder AddWireMock(this IDistrib var resourceBuilder = builder .AddResource(wireMockContainerResource) .WithImage(DefaultLinuxImage) + .WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort) .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) + foreach (var httpPort in arguments.HttpPorts) { - resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort); - } - else - { - foreach (var httpPort in arguments.HttpPorts) - { - resourceBuilder = resourceBuilder.WithHttpEndpoint(port: httpPort, targetPort: WireMockServerArguments.HttpContainerPort); - } + resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: httpPort, name: $"http-{httpPort}"); } if (!string.IsNullOrEmpty(arguments.MappingsPath)) @@ -109,7 +130,10 @@ public static IResourceBuilder AddWireMock(this IDistrib /// 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) + public static IResourceBuilder AddWireMock( + this IDistributedApplicationBuilder builder, + string name, + Action callback) { Guard.NotNull(builder); Guard.NotNullOrWhiteSpace(name); @@ -203,25 +227,6 @@ public static IResourceBuilder WithApiMappingBuilder(thi return wiremock; } - /// - /// Adds another URL to the WireMock container. By default, the WireMock container will listen on http://*:80. - /// - /// This method can be used to also host the WireMock container on another port or protocol (like grpc). - /// - /// grpc://*:9090 - /// A reference to the . - public static IResourceBuilder AddUrl(this IResourceBuilder wiremock, string url) - { - if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port)) - { - throw new ArgumentException("The URL is not valid.", nameof(url)); - } - - wiremock.Resource.Arguments.WithAdditionalUrlWithPort(url, port); - - return wiremock; - } - /// /// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting. /// This requires installation of the WireMockInspector tool. diff --git a/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs index 5c282c233..a02e06d2c 100644 --- a/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs +++ b/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs @@ -55,7 +55,7 @@ public void Parse(string[] arguments, IDictionary? environment = null) // Now also parse environment if (environment != null) { - foreach (string key in environment.Keys) + foreach (var key in environment.Keys.OfType()) { if (key.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && environment.TryGetStringValue(key, out var value)) { diff --git a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs index 90c2b3357..0dba1c218 100644 --- a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs @@ -153,7 +153,7 @@ private static void ParsePortSettings(WireMockServerSettings settings, SimpleSet } else if (settings.HostingScheme is null) { - settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]); + settings.Urls = parser.GetValues(nameof(WireMockServerSettings.Urls), defaultValue: ["http://*:9091/"]); } } From f33791f37c79eb56028ebc8d1f78e6865eca4e11 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Mon, 24 Nov 2025 19:12:21 +0100 Subject: [PATCH 4/9] ok? --- examples-Aspire/AspireApp1.AppHost/Program.cs | 1 + .../WireMockServerArguments.cs | 20 +--- .../WireMockServerBuilderExtensions.cs | 28 ++++- src/WireMock.Net.Minimal/Util/PortUtils.cs | 12 +-- .../WireMockServerArgumentsTests.cs | 2 +- .../WireMockServerBuilderExtensionsTests.cs | 102 +++++++++++++++++- .../WireMock.Net.Tests/Util/PortUtilsTests.cs | 24 ++--- 7 files changed, 149 insertions(+), 40 deletions(-) diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index cfd42e126..638fd6056 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -9,6 +9,7 @@ IResourceBuilder apiService = builder //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort) .AddWireMock("apiservice", ["http://*:8080", "grpc://*:9090"]) + .AsHttp2Service() .WithMappingsPath(mappingsPath) .WithReadStaticMappings() .WithWatchStaticMappings() diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index 06702409e..c753647f8 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -27,6 +27,11 @@ public class WireMockServerArguments /// public List HttpPorts { get; set; } = []; + /// + /// Additional Urls on which WireMock listens. + /// + public List AdditionalUrls { get; set; } = []; + /// /// The admin username. /// @@ -68,16 +73,6 @@ public class WireMockServerArguments /// public Func? ApiMappingBuilder { get; set; } - /// - /// Use HTTP 2 (used for Grpc). - /// - public bool UseHttp2 { get; set; } - - /// - /// Aadditional Urls on which WireMock listens. - /// - public List AdditionalUrls { get; set; } = []; - /// /// Add an additional Url on which WireMock listens. /// @@ -117,11 +112,6 @@ public string[] GetArgs() Add(args, "--WatchStaticMappingsInSubdirectories", "true"); } - if (UseHttp2) - { - Add(args, "--UseHttp2", "true"); - } - if (AdditionalUrls.Count > 0) { Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}"); diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index 62a7a9ff3..3331843ac 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -97,14 +97,36 @@ public static IResourceBuilder AddWireMock(this IDistrib var resourceBuilder = builder .AddResource(wireMockContainerResource) .WithImage(DefaultLinuxImage) - .WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort) .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(); - foreach (var httpPort in arguments.HttpPorts) + if (arguments.HttpPorts.Count == 0) { - resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: httpPort, name: $"http-{httpPort}"); + 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)) diff --git a/src/WireMock.Net.Minimal/Util/PortUtils.cs b/src/WireMock.Net.Minimal/Util/PortUtils.cs index 1cc2aad8d..d83fc3a01 100644 --- a/src/WireMock.Net.Minimal/Util/PortUtils.cs +++ b/src/WireMock.Net.Minimal/Util/PortUtils.cs @@ -84,22 +84,22 @@ public static IReadOnlyList FindFreeTcpPorts(int count) } /// - /// Extract the isHttps, isHttp2, protocol, host and port from a URL. + /// Extract the isHttps, isHttp2, scheme, host and port from a URL. /// - public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port) + public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? scheme, [NotNullWhen(true)] out string? host, out int port) { isHttps = false; isHttp2 = false; - protocol = null; + scheme = null; host = null; port = 0; var match = UrlDetailsRegex.Match(url); if (match.Success) { - protocol = match.Groups["proto"].Value; - isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase) || protocol.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase); - isHttp2 = protocol.StartsWith("grpc", StringComparison.OrdinalIgnoreCase); + scheme = match.Groups["proto"].Value; + isHttps = scheme.StartsWith("https", StringComparison.OrdinalIgnoreCase) || scheme.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase); + isHttp2 = scheme.StartsWith("grpc", StringComparison.OrdinalIgnoreCase); host = match.Groups["host"].Value; return int.TryParse(match.Groups["port"].Value, out port); diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs index 15a4ca3c0..60571ab5f 100644 --- a/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs +++ b/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs @@ -13,7 +13,7 @@ public void DefaultValues_ShouldBeSetCorrectly() var args = new WireMockServerArguments(); // Assert - args.HttpPort.Should().BeNull(); + args.HttpPorts.Should().BeEmpty(); args.AdminUsername.Should().BeNull(); args.AdminPassword.Should().BeNull(); args.ReadStaticMappings.Should().BeFalse(); diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs index 88ea28a05..bb3d6ebdd 100644 --- a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs +++ b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using FluentAssertions; using Moq; +using WireMock.Util; namespace WireMock.Net.Aspire.Tests; @@ -40,7 +41,21 @@ public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException() } [Fact] - public void AddWireMock() + public void AddWireMock_WithInvalidAdditionalUrls_ShouldThrowArgumentException() + { + // Arrange + string[] invalidUrls = { "err" }; + var builder = Mock.Of(); + + // Act + Action act = () => builder.AddWireMock("ValidName", invalidUrls); + + // Assert + act.Should().Throw().WithMessage("The URL err is not valid."); + } + + [Fact] + public void AddWireMockWithPort() { // Arrange var name = $"apiservice{Guid.NewGuid()}"; @@ -65,7 +80,7 @@ public void AddWireMock() ReadStaticMappings = true, WatchStaticMappings = false, MappingsPath = null, - HttpPort = port + HttpPorts = [port] }); wiremock.Resource.Annotations.Should().HaveCount(6); @@ -90,9 +105,90 @@ public void AddWireMock() )); wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); - wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + } + + [Fact] + public void AddWireMockWithAdditionalUrls() + { + // Arrange + var name = $"apiservice{Guid.NewGuid()}"; + var freePorts = PortUtils.FindFreeTcpPorts(2).ToList(); + string[] additionalUrls = { $"http://*:{freePorts[0]}", $"grpc://*:{freePorts[1]}" }; + const string username = "admin"; + const string password = "test"; + var builder = DistributedApplication.CreateBuilder(); + + // Act + var wiremock = builder + .AddWireMock(name, additionalUrls) + .WithAdminUserNameAndPassword(username, password) + .WithReadStaticMappings(); + + // Assert + wiremock.Resource.Should().NotBeNull(); + wiremock.Resource.Name.Should().Be(name); + wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments + { + AdminPassword = password, + AdminUsername = username, + ReadStaticMappings = true, + WatchStaticMappings = false, + MappingsPath = null, + HttpPorts = freePorts, + AdditionalUrls = additionalUrls.ToList() + }); + wiremock.Resource.Annotations.Should().HaveCount(9); + + var containerImageAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault(); + containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation + { + Image = "sheyenrath/wiremock.net-alpine", + Registry = null, + Tag = "latest" + }); + + var endpointAnnotations = wiremock.Resource.Annotations.OfType().ToArray(); + endpointAnnotations.Should().HaveCount(3); + var endpointAnnotationForHttp80 = endpointAnnotations[0]; + endpointAnnotationForHttp80.Should().BeEquivalentTo(new EndpointAnnotation( + protocol: ProtocolType.Tcp, + uriScheme: "http", + transport: null, + name: null, + port: null, + targetPort: 80, + isExternal: null, + isProxied: true + )); + var endpointAnnotationForHttpFreePort = endpointAnnotations[1]; + endpointAnnotationForHttpFreePort.Should().BeEquivalentTo(new EndpointAnnotation( + protocol: ProtocolType.Tcp, + uriScheme: "http", + transport: null, + name: $"http-{freePorts[0]}", + port: freePorts[0], + targetPort: freePorts[0], + isExternal: null, + isProxied: true + )); + + var endpointAnnotationForGrpcFreePort = endpointAnnotations[2]; + endpointAnnotationForGrpcFreePort.Should().BeEquivalentTo(new EndpointAnnotation( + protocol: ProtocolType.Tcp, + uriScheme: "grpc", + transport: null, + name: $"grpc-{freePorts[1]}", + port: freePorts[1], + targetPort: freePorts[1], + isExternal: null, + isProxied: true + )); + + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); + wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull(); } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs index 90b7777fe..2915e75f0 100644 --- a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs +++ b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs @@ -15,13 +15,13 @@ public void PortUtils_TryExtract_InvalidUrl_Returns_False() var url = "test"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeFalse(); isHttps.Should().BeFalse(); isGrpc.Should().BeFalse(); - proto.Should().BeNull(); + scheme.Should().BeNull(); host.Should().BeNull(); port.Should().Be(default(int)); } @@ -33,13 +33,13 @@ public void PortUtils_TryExtract_UrlIsMissingPort_Returns_False() var url = "http://0.0.0.0"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeFalse(); isHttps.Should().BeFalse(); isGrpc.Should().BeFalse(); - proto.Should().BeNull(); + scheme.Should().BeNull(); host.Should().BeNull(); port.Should().Be(default(int)); } @@ -51,13 +51,13 @@ public void PortUtils_TryExtract_Http_Returns_True() var url = "http://wiremock.net:1234"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeFalse(); isGrpc.Should().BeFalse(); - proto.Should().Be("http"); + scheme.Should().Be("http"); host.Should().Be("wiremock.net"); port.Should().Be(1234); } @@ -69,13 +69,13 @@ public void PortUtils_TryExtract_Https_Returns_True() var url = "https://wiremock.net:5000"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeTrue(); isGrpc.Should().BeFalse(); - proto.Should().Be("https"); + scheme.Should().Be("https"); host.Should().Be("wiremock.net"); port.Should().Be(5000); } @@ -87,13 +87,13 @@ public void PortUtils_TryExtract_Grpc_Returns_True() var url = "grpc://wiremock.net:1234"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeFalse(); isGrpc.Should().BeTrue(); - proto.Should().Be("grpc"); + scheme.Should().Be("grpc"); host.Should().Be("wiremock.net"); port.Should().Be(1234); } @@ -105,13 +105,13 @@ public void PortUtils_TryExtract_Https0_0_0_0_Returns_True() var url = "https://0.0.0.0:5000"; // Act - var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeTrue(); isGrpc.Should().BeFalse(); - proto.Should().Be("https"); + scheme.Should().Be("https"); host.Should().Be("0.0.0.0"); port.Should().Be(5000); } From dbe94aabc9b8f57edc800b4a2eb4cf6f2a298c4c Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 29 Nov 2025 08:12:54 +0100 Subject: [PATCH 5/9] WireMockServerArguments --- examples-Aspire/AspireApp1.AppHost/Program.cs | 19 ++++++++++++---- .../WireMockServerArguments.cs | 22 +++++++++++++------ .../WireMockServerBuilderExtensions.cs | 12 ++-------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index 638fd6056..80d11a5f2 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -6,9 +6,20 @@ var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings"); -IResourceBuilder apiService = builder +IResourceBuilder apiService1 = builder //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort) - .AddWireMock("apiservice", ["http://*:8080", "grpc://*:9090"]) + .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091") + .AsHttp2Service() + .WithMappingsPath(mappingsPath) + .WithReadStaticMappings() + .WithWatchStaticMappings() + .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync); + +IResourceBuilder apiService2 = builder + .AddWireMock("apiservice2", args => + { + args.WithAdditionalUrls("http://*:8081", "grpc://*:9091"); + }) .AsHttp2Service() .WithMappingsPath(mappingsPath) .WithReadStaticMappings() @@ -47,7 +58,7 @@ builder.AddProject("webfrontend") .WithExternalHttpEndpoints() - .WithReference(apiService) - .WaitFor(apiService); + .WithReference(apiService2) + .WaitFor(apiService2); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index c753647f8..ef3fca257 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Stef.Validation; using WireMock.Client.Builders; +using WireMock.Util; // ReSharper disable once CheckNamespace namespace Aspire.Hosting; @@ -22,7 +23,7 @@ public class WireMockServerArguments private const string DefaultLogger = "WireMockConsoleLogger"; /// - /// The HTTP port where WireMock.Net is listening. + /// The HTTP ports where WireMock.Net is listening on. /// If not defined, .NET Aspire automatically assigns a random port. /// public List HttpPorts { get; set; } = []; @@ -74,14 +75,21 @@ public class WireMockServerArguments public Func? ApiMappingBuilder { get; set; } /// - /// Add an additional Url on which WireMock listens. + /// Add an additional Urls on which WireMock should listen. /// - /// The url to add. - /// The port to add. - internal void WithAdditionalUrlWithPort(string url, int port) + /// The additional urls which the WireMock Server should listen on. + public void WithAdditionalUrls(params string[] additionalUrls) { - AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url)); - HttpPorts.Add(port); + foreach (var url in additionalUrls) + { + if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port)) + { + throw new ArgumentException($"The URL '{url}' is not valid."); + } + + AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url)); + HttpPorts.Add(port); + } } /// diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index 3331843ac..8b844ab20 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -49,7 +49,7 @@ public static IResourceBuilder AddWireMock(this IDistrib /// /// 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 listens on. + /// 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) { @@ -59,15 +59,7 @@ public static IResourceBuilder AddWireMock(this IDistrib return builder.AddWireMock(name, serverArguments => { - foreach (var url in additionalUrls) - { - if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port)) - { - throw new ArgumentException($"The URL {url} is not valid."); - } - - serverArguments.WithAdditionalUrlWithPort(url, port); - } + serverArguments.WithAdditionalUrls(additionalUrls); }); } From ccf8b9af9413a9b2f4f0bc3efd07909950afef37 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 29 Nov 2025 08:47:20 +0100 Subject: [PATCH 6/9] fx --- .../WireMockServerBuilderExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs index bb3d6ebdd..8a2d61744 100644 --- a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs +++ b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs @@ -51,7 +51,7 @@ public void AddWireMock_WithInvalidAdditionalUrls_ShouldThrowArgumentException() Action act = () => builder.AddWireMock("ValidName", invalidUrls); // Assert - act.Should().Throw().WithMessage("The URL err is not valid."); + act.Should().Throw().WithMessage("The URL 'err' is not valid."); } [Fact] From 036284cb3c1542668e802e8016a266e928c34c72 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 29 Nov 2025 15:33:06 +0100 Subject: [PATCH 7/9] AddProtoDefinition --- .../AspireApp1.AppHost.csproj | 6 ++++++ examples-Aspire/AspireApp1.AppHost/Program.cs | 10 ++++++++- .../__admin/mappings/greet.proto | 21 +++++++++++++++++++ .../WireMockServerArguments.cs | 18 ++++++++++++++++ .../WireMockServerLifecycleHook.cs | 2 ++ .../WireMockServerResource.cs | 21 +++++++++++++++++++ 6 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto diff --git a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj index f05544701..33ac39810 100644 --- a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj +++ b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj @@ -21,4 +21,10 @@ + + + PreserveNewest + + + diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index 80d11a5f2..048d1e788 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -19,6 +19,7 @@ .AddWireMock("apiservice2", args => { args.WithAdditionalUrls("http://*:8081", "grpc://*:9091"); + args.AddProtoDefinition("my-greeter", ReadFile("greet.proto")); }) .AsHttp2Service() .WithMappingsPath(mappingsPath) @@ -61,4 +62,11 @@ .WithReference(apiService2) .WaitFor(apiService2); -await builder.Build().RunAsync(); \ No newline at end of file +await builder.Build().RunAsync(); + +return; + +static string ReadFile(string filename) +{ + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", filename)); +} \ No newline at end of file diff --git a/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto new file mode 100644 index 000000000..f4e1ead02 --- /dev/null +++ b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; + enum PhoneType { + none = 0; + mobile = 1; + home = 2; + } + PhoneType phoneType = 2; +} \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index ef3fca257..4c233c3ed 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -74,6 +74,11 @@ public class WireMockServerArguments /// public Func? ApiMappingBuilder { get; set; } + /// + /// Grpc ProtoDefinitions. + /// + public Dictionary ProtoDefinitions { get; set; } = []; + /// /// Add an additional Urls on which WireMock should listen. /// @@ -92,6 +97,19 @@ public void WithAdditionalUrls(params string[] additionalUrls) } } + /// + /// Add a Grpc ProtoDefinition at server-level. + /// + /// Unique identifier for the ProtoDefinition. + /// The ProtoDefinition as text. + public void AddProtoDefinition(string id, params string[] protoDefinition) + { + Guard.NotNullOrWhiteSpace(id); + Guard.NotNullOrEmpty(protoDefinition); + + ProtoDefinitions[id] = protoDefinition; + } + /// /// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server. /// diff --git a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs index 57df9ee81..161bd4890 100644 --- a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs +++ b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs @@ -34,6 +34,8 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token); + await wireMockServerResource.CallAddProtoDefinitionsAsync(_linkedCts.Token); + wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token); } }, _linkedCts.Token); diff --git a/src/WireMock.Net.Aspire/WireMockServerResource.cs b/src/WireMock.Net.Aspire/WireMockServerResource.cs index 59e187993..75d803c19 100644 --- a/src/WireMock.Net.Aspire/WireMockServerResource.cs +++ b/src/WireMock.Net.Aspire/WireMockServerResource.cs @@ -70,6 +70,27 @@ internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellat ApiMappingState = WireMockMappingState.Submitted; } + internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationToken) + { + _logger?.LogInformation("Calling AdminApi to add GRPC ProtoDefinition at server level to WireMock.Net"); + + foreach (var (id, protoDefinitions) in Arguments.ProtoDefinitions) + { + _logger?.LogInformation("Adding ProtoDefinition {Id}", id); + foreach (var protoDefinition in protoDefinitions) + { + try + { + await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition); + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", id); + } + } + } + } + internal void StartWatchingStaticMappings(CancellationToken cancellationToken) { if (!Arguments.WatchStaticMappings || string.IsNullOrEmpty(Arguments.MappingsPath)) From eccbd87f0df747425f49fd0041cbb730e1f8fb75 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 29 Nov 2025 17:51:09 +0100 Subject: [PATCH 8/9] ... --- .../AspireApp1.AppHost.csproj | 11 +++-- examples-Aspire/AspireApp1.AppHost/Program.cs | 35 +++++++--------- .../873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json | 0 .../__admin/mappings/protobuf-mapping-4.json | 40 +++++++++++++++++++ .../WireMockServerArguments.cs | 8 ++-- .../WireMockServerBuilderExtensions.cs | 30 ++++++++++---- .../WireMockServerLifecycleHook.cs | 4 +- .../WireMockServerResource.cs | 2 +- .../TestcontainersTests.Grpc.cs | 2 +- 9 files changed, 92 insertions(+), 40 deletions(-) rename examples-Aspire/AspireApp1.AppHost/{WireMockMappings => __admin/mappings}/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json (100%) create mode 100644 examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json diff --git a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj index 33ac39810..cf43ca011 100644 --- a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj +++ b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj @@ -22,9 +22,12 @@ - - PreserveNewest - + + PreserveNewest + + + PreserveNewest + - + \ No newline at end of file diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index 048d1e788..927114d78 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -4,27 +4,27 @@ // IResourceBuilder apiService = builder.AddProject("apiservice"); -var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings"); +var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings"); -IResourceBuilder apiService1 = builder - //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort) - .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091") - .AsHttp2Service() - .WithMappingsPath(mappingsPath) - .WithReadStaticMappings() - .WithWatchStaticMappings() - .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync); +//IResourceBuilder apiService1 = builder +// //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort) +// .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091") +// .AsHttp2Service() +// .WithMappingsPath(mappingsPath) +// .WithReadStaticMappings() +// .WithWatchStaticMappings() +// .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync); IResourceBuilder apiService2 = builder - .AddWireMock("apiservice2", args => + .AddWireMock("apiservice", args => { - args.WithAdditionalUrls("http://*:8081", "grpc://*:9091"); - args.AddProtoDefinition("my-greeter", ReadFile("greet.proto")); + args.WithAdditionalUrls("http://*:8081", "grpc://*:9093"); }) .AsHttp2Service() + .WithProtoDefinition("my-greeter", await File.ReadAllTextAsync(Path.Combine(mappingsPath, "greet.proto"))) .WithMappingsPath(mappingsPath) .WithReadStaticMappings() - .WithWatchStaticMappings() + .WithWatchStaticMappings() .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync); //var apiServiceUsedForDocs = builder @@ -62,11 +62,4 @@ .WithReference(apiService2) .WaitFor(apiService2); -await builder.Build().RunAsync(); - -return; - -static string ReadFile(string filename) -{ - return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", filename)); -} \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/examples-Aspire/AspireApp1.AppHost/WireMockMappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json similarity index 100% rename from examples-Aspire/AspireApp1.AppHost/WireMockMappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json rename to examples-Aspire/AspireApp1.AppHost/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json diff --git a/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json new file mode 100644 index 000000000..930dc941f --- /dev/null +++ b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json @@ -0,0 +1,40 @@ +{ + "Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0004", + "Title": "ProtoBuf Mapping 4", + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/greet.Greeter/SayHello", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "Matcher": { + "Name": "ProtoBufMatcher", + "ProtoBufMessageType": "greet.HelloRequest" + } + } + }, + "Response": { + "BodyAsJson": { + "message": "hello {{request.BodyAsJson.name}} {{request.method}}" + }, + "UseTransformer": true, + "TransformerType": "Handlebars", + "TransformerReplaceNodeOptions": "EvaluateAndTryToConvert", + "Headers": { + "Content-Type": "application/grpc" + }, + "TrailingHeaders": { + "grpc-status": "0" + }, + "ProtoBufMessageType": "greet.HelloReply" + }, + "ProtoDefinition": "my-greeter" +} \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs index 4c233c3ed..df2e92101 100644 --- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs +++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs @@ -101,13 +101,13 @@ public void WithAdditionalUrls(params string[] additionalUrls) /// Add a Grpc ProtoDefinition at server-level. /// /// Unique identifier for the ProtoDefinition. - /// The ProtoDefinition as text. - public void AddProtoDefinition(string id, params string[] protoDefinition) + /// The ProtoDefinition as text. + public void AddProtoDefinition(string id, params string[] protoDefinitions) { Guard.NotNullOrWhiteSpace(id); - Guard.NotNullOrEmpty(protoDefinition); + Guard.NotNullOrEmpty(protoDefinitions); - ProtoDefinitions[id] = protoDefinition; + ProtoDefinitions[id] = protoDefinitions; } /// diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs index 8b844ab20..2f32aa8f5 100644 --- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs +++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs @@ -241,6 +241,22 @@ public static IResourceBuilder WithApiMappingBuilder(thi 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.AddProtoDefinition(id, protoDefinitions); + + wiremock.ApplicationBuilder.Services.TryAddLifecycleHook(); + + return wiremock; + } + /// /// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting. /// This requires installation of the WireMockInspector tool. @@ -248,11 +264,11 @@ public static IResourceBuilder WithApiMappingBuilder(thi /// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources /// /// - /// The . - /// - public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder builder) + /// The . + /// A reference to the . + public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder wiremock) { - Guard.NotNull(builder); + Guard.NotNull(wiremock); CommandOptions commandOptions = new() { @@ -262,13 +278,13 @@ public static IResourceBuilder WithWireMockInspectorComm IconVariant = IconVariant.Filled }; - builder.WithCommand( + wiremock.WithCommand( name: "wiremock-inspector", displayName: "WireMock Inspector", - executeCommand: _ => OnRunOpenInspectorCommandAsync(builder), + executeCommand: _ => OnRunOpenInspectorCommandAsync(wiremock), commandOptions: commandOptions); - return builder; + return wiremock; } private static Task OnRunOpenInspectorCommandAsync(IResourceBuilder builder) diff --git a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs index 161bd4890..192f59d2c 100644 --- a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs +++ b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs @@ -32,10 +32,10 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token); - await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token); - await wireMockServerResource.CallAddProtoDefinitionsAsync(_linkedCts.Token); + await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token); + wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token); } }, _linkedCts.Token); diff --git a/src/WireMock.Net.Aspire/WireMockServerResource.cs b/src/WireMock.Net.Aspire/WireMockServerResource.cs index 75d803c19..1d75b72d5 100644 --- a/src/WireMock.Net.Aspire/WireMockServerResource.cs +++ b/src/WireMock.Net.Aspire/WireMockServerResource.cs @@ -81,7 +81,7 @@ internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationT { try { - await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition); + await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken); } catch (Exception ex) { diff --git a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs index 12b903b2b..7137f5cc9 100644 --- a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs @@ -56,7 +56,7 @@ public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls1() var grpcPort = wireMockContainer.GetMappedPublicPort(9090); grpcPort.Should().BeGreaterThan(0); - var grpcUrl = wireMockContainer.GetMappedPublicUrl(80); + var grpcUrl = wireMockContainer.GetMappedPublicUrl(9090); grpcUrl.Should().StartWith("http://"); var adminClient = wireMockContainer.CreateWireMockAdminClient(); From 96811db9d25c40fc4493f99b64464596058e37de Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 30 Nov 2025 08:45:43 +0100 Subject: [PATCH 9/9] FIX --- examples-Aspire/AspireApp1.AppHost/Program.cs | 3 +-- .../Admin/Mappings/StatusModel.cs | 9 +++++++ .../WireMockServerLifecycleHook.cs | 3 ++- .../WireMockServerResource.cs | 18 +++++++++++-- .../WireMockContainer.cs | 13 ++++++++-- .../WireMockContainerBuilder.cs | 5 ++-- .../TestcontainersTests.Grpc.cs | 25 +++++++++++++++++++ 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs index 927114d78..70088d0ac 100644 --- a/examples-Aspire/AspireApp1.AppHost/Program.cs +++ b/examples-Aspire/AspireApp1.AppHost/Program.cs @@ -23,8 +23,7 @@ .AsHttp2Service() .WithProtoDefinition("my-greeter", await File.ReadAllTextAsync(Path.Combine(mappingsPath, "greet.proto"))) .WithMappingsPath(mappingsPath) - .WithReadStaticMappings() - .WithWatchStaticMappings() + .WithWatchStaticMappings() .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync); //var apiServiceUsedForDocs = builder diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs index 2259bc599..45e73528d 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs @@ -24,4 +24,13 @@ public class StatusModel /// The error message. /// public string? Error { get; set; } + + /// + /// Returns a string that represents the current status model, including its unique identifier, status, and error information. + /// + /// A string containing the values of the Guid, Status, and Error properties formatted for display. + public override string ToString() + { + return $"StatusModel [Guid={Guid}, Status={Status}, Error={Error}]"; + } } \ No newline at end of file diff --git a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs index 192f59d2c..024724dfe 100644 --- a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs +++ b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs @@ -1,5 +1,6 @@ // Copyright © WireMock.Net +using System.Diagnostics; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; @@ -28,7 +29,7 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C wireMockServerResource.SetLogger(loggerFactory.CreateLogger()); var endpoint = wireMockServerResource.GetEndpoint(); - System.Diagnostics.Debug.Assert(endpoint.IsAllocated); + Debug.Assert(endpoint.IsAllocated); await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token); diff --git a/src/WireMock.Net.Aspire/WireMockServerResource.cs b/src/WireMock.Net.Aspire/WireMockServerResource.cs index 1d75b72d5..d9529c112 100644 --- a/src/WireMock.Net.Aspire/WireMockServerResource.cs +++ b/src/WireMock.Net.Aspire/WireMockServerResource.cs @@ -81,7 +81,8 @@ internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationT { try { - await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken); + var status = await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken); + _logger?.LogInformation("ProtoDefinition '{Id}' added with status: {Status}.", id, status.Status); } catch (Exception ex) { @@ -89,6 +90,12 @@ internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationT } } } + + // Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382 + if (Arguments.ProtoDefinitions.Count > 0) + { + await ReloadStaticMappingsAsync(default); + } } internal void StartWatchingStaticMappings(CancellationToken cancellationToken) @@ -135,9 +142,16 @@ private IWireMockAdminApi CreateWireMockAdminApi() private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args) { _logger?.LogInformation("MappingFile created, changed or deleted: '{FullPath}'. Triggering ReloadStaticMappings.", args.FullPath); + + await ReloadStaticMappingsAsync(default); + } + + private async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken) + { try { - await AdminApi.Value.ReloadStaticMappingsAsync(); + var status = await AdminApi.Value.ReloadStaticMappingsAsync(cancellationToken); + _logger?.LogInformation("ReloadStaticMappings called with status: {Status}.", status); } catch (Exception ex) { diff --git a/src/WireMock.Net.Testcontainers/WireMockContainer.cs b/src/WireMock.Net.Testcontainers/WireMockContainer.cs index a01a6add4..002a04694 100644 --- a/src/WireMock.Net.Testcontainers/WireMockContainer.cs +++ b/src/WireMock.Net.Testcontainers/WireMockContainer.cs @@ -156,7 +156,8 @@ public async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken try { - await _adminApi.ReloadStaticMappingsAsync(cancellationToken); + var result = await _adminApi.ReloadStaticMappingsAsync(cancellationToken); + Logger.LogInformation("ReloadStaticMappings result: {Result}", result); } catch (Exception ex) { @@ -231,7 +232,8 @@ private async Task CallAdditionalActionsAfterStartedAsync() { try { - await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition); + var result = await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition); + Logger.LogInformation("AddProtoDefinition '{Id}' result: {Result}", kvp.Key, result); } catch (Exception ex) { @@ -239,6 +241,12 @@ private async Task CallAdditionalActionsAfterStartedAsync() } } } + + // Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382 + if (_configuration.ProtoDefinitions.Count > 0) + { + await ReloadStaticMappingsAsync(); + } } private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args) @@ -246,6 +254,7 @@ private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArg try { await ReloadStaticMappingsAsync(args.FullPath); + Logger.LogInformation("ReloadStaticMappings triggered from file change: '{FullPath}'.", args.FullPath); } catch (Exception ex) { diff --git a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs index ef634ff98..16cf55dd5 100644 --- a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs +++ b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs @@ -112,6 +112,7 @@ public WireMockContainerBuilder WithWatchStaticMappings(bool includeSubDirectori { DockerResourceConfiguration.WithWatchStaticMappings(includeSubDirectories); return + WithCommand("--ReadStaticMappings true"). WithCommand("--WatchStaticMappings true"). WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories); } @@ -129,9 +130,7 @@ public WireMockContainerBuilder WithMappings(string path, bool includeSubDirecto DockerResourceConfiguration.WithStaticMappingsPath(path); - return - WithReadStaticMappings(). - WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories); + return WithWatchStaticMappings(includeSubDirectories); } /// diff --git a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs index 7137f5cc9..0afa831f2 100644 --- a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs @@ -149,6 +149,18 @@ public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_Usin await StopAsync(wireMockContainer); } + [Fact] + public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient_AndWithWatchStaticMappings() + { + var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync(); + + var reply = await When_GrpcClient_Calls_SayHelloAsync(wireMockContainer); + + Then_ReplyMessage_Should_BeCorrect(reply); + + await StopAsync(wireMockContainer); + } + private static async Task Given_WireMockContainerIsStartedForHttpAndGrpcAsync() { var wireMockContainer = new WireMockContainerBuilder() @@ -172,6 +184,19 @@ private static async Task Given_WireMockContainerWithProtoDef return wireMockContainer; } + private static async Task Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync() + { + var wireMockContainer = new WireMockContainerBuilder() + .AddUrl("grpc://*:9090") + .AddProtoDefinition("my-greeter", ReadFile("greet.proto")) + .WithMappings(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings")) + .Build(); + + await wireMockContainer.StartAsync(); + + return wireMockContainer; + } + private static async Task Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockContainer wireMockContainer, string filename) { var mappingsJson = ReadFile(filename);