diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs
index a6c7ce1c9..75c4bb957 100644
--- a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs
+++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs
@@ -55,12 +55,17 @@ public class MappingModel
/// In case the value is null state will not be changed.
///
public string? SetStateTo { get; set; }
-
+
///
/// The number of times this match should be matched before the state will be changed to the specified one.
///
public int? TimesInSameState { get; set; }
+ ///
+ /// Value to determine if the mapping is disabled. Defaults to null (not disabled).
+ ///
+ public bool? IsDisabled { get; set; }
+
///
/// The request model.
///
@@ -100,7 +105,7 @@ public class MappingModel
///
public object? Data { get; set; }
- ///
+ ///
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
///
public double? Probability { get; set; }
diff --git a/src/WireMock.Net.Minimal/Mapping.cs b/src/WireMock.Net.Minimal/Mapping.cs
index 181c7fa99..b4441ce99 100644
--- a/src/WireMock.Net.Minimal/Mapping.cs
+++ b/src/WireMock.Net.Minimal/Mapping.cs
@@ -62,6 +62,9 @@ public class Mapping : IMapping
///
public bool IsProxy => Provider is ProxyAsyncResponseProvider;
+ ///
+ public bool IsDisabled { get; set; }
+
///
public bool LogMapping => Provider is not (DynamicResponseProvider or DynamicAsyncResponseProvider);
diff --git a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs
index 0750afce2..97d1fc58b 100644
--- a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs
+++ b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs
@@ -19,6 +19,7 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
var possibleMappings = new List();
var mappings = _options.Mappings.Values
+ .Where(m => !m.IsDisabled)
.Where(m => m.TimeSettings.IsValid())
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
.ToArray();
diff --git a/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs b/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs
index 90b3d246d..f26c4ddfb 100644
--- a/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs
+++ b/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs
@@ -275,6 +275,7 @@ public MappingModel ToMappingModel(IMapping mapping)
TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null,
Data = mapping.Data,
Probability = mapping.Probability,
+ IsDisabled = mapping.IsDisabled ? true : null,
Request = new RequestModel
{
Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel
diff --git a/src/WireMock.Net.Minimal/Server/IRespondWithAProvider.cs b/src/WireMock.Net.Minimal/Server/IRespondWithAProvider.cs
index a26e45240..c76696f3c 100644
--- a/src/WireMock.Net.Minimal/Server/IRespondWithAProvider.cs
+++ b/src/WireMock.Net.Minimal/Server/IRespondWithAProvider.cs
@@ -234,6 +234,13 @@ IRespondWithAProvider WithWebhook(
/// The .
IRespondWithAProvider WithProbability(double probability);
+ ///
+ /// Define whether this mapping is disabled. Defaults to false.
+ ///
+ /// Whether this mapping is disabled.
+ /// The .
+ IRespondWithAProvider WithIsDisabled(bool isDisabled);
+
///
/// Define a Grpc ProtoDefinition which is used for the request and the response.
/// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer.
diff --git a/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs b/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
index 65422f6cc..499caf668 100644
--- a/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
+++ b/src/WireMock.Net.Minimal/Server/RespondWithAProvider.cs
@@ -37,6 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
private int _timesInSameState = 1;
private bool? _useWebhookFireAndForget;
private double? _probability;
+ private bool _isDisabled = false;
private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
public Guid Guid { get; private set; }
@@ -108,6 +109,11 @@ public void RespondWith(IResponseProvider provider)
mapping.WithProbability(_probability.Value);
}
+ if (_isDisabled)
+ {
+ mapping.IsDisabled = true;
+ }
+
if (ProtoDefinition != null)
{
mapping.WithProtoDefinition(ProtoDefinition.Value);
@@ -354,6 +360,13 @@ public IRespondWithAProvider WithProbability(double probability)
return this;
}
+ ///
+ public IRespondWithAProvider WithIsDisabled(bool isDisabled)
+ {
+ _isDisabled = isDisabled;
+ return this;
+ }
+
///
public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId)
{
diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
index 80e12db65..48a4990c1 100644
--- a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
+++ b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs
@@ -57,6 +57,8 @@ public AdminPaths(WireMockServerSettings settings)
public string OpenApi => $"{_prefix}/openapi";
public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
+ public RegexMatcher MappingsGuidEnablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/enable$");
+ public RegexMatcher MappingsGuidDisablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/disable$");
public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
@@ -100,6 +102,12 @@ private void InitAdmin()
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, AdminRequestContentTypeJson)).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));
+ // __admin/mappings/{guid}/enable
+ Given(Request.Create().WithPath(_adminPaths.MappingsGuidEnablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingEnable));
+
+ // __admin/mappings/{guid}/disable
+ Given(Request.Create().WithPath(_adminPaths.MappingsGuidDisablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDisable));
+
// __admin/mappings/code/{guid}
Given(Request.Create().WithPath(_adminPaths.MappingsCodeGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingCodeGet));
@@ -426,6 +434,47 @@ private static bool TryParseGuidFromRequestMessage(IRequestMessage requestMessag
var lastPart = requestMessage.Path.Split('/').LastOrDefault();
return Guid.TryParse(lastPart, out guid);
}
+
+ private static bool TryParseGuidFromSecondToLastSegment(IRequestMessage requestMessage, out Guid guid)
+ {
+ var parts = requestMessage.Path.Split('/');
+ if (parts.Length >= 2 && Guid.TryParse(parts[parts.Length - 2], out guid))
+ return true;
+ guid = Guid.Empty;
+ return false;
+ }
+
+ private IResponseMessage MappingEnable(HttpContext _, IRequestMessage requestMessage)
+ {
+ if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
+ {
+ var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
+ if (mapping != null)
+ {
+ mapping.IsDisabled = false;
+ return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping enabled", guid);
+ }
+ }
+
+ _settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
+ return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
+ }
+
+ private IResponseMessage MappingDisable(HttpContext _, IRequestMessage requestMessage)
+ {
+ if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
+ {
+ var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
+ if (mapping != null)
+ {
+ mapping.IsDisabled = true;
+ return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping disabled", guid);
+ }
+ }
+
+ _settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
+ return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
+ }
#endregion Mapping/{guid}
#region Mappings
diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs
index 4cff565dc..fa27c1a2e 100644
--- a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs
+++ b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs
@@ -120,6 +120,11 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode
respondProvider.WithProbability(mappingModel.Probability.Value);
}
+ if (mappingModel.IsDisabled == true)
+ {
+ respondProvider.WithIsDisabled(true);
+ }
+
// ProtoDefinition is defined at Mapping level
if (mappingModel.ProtoDefinition != null)
{
diff --git a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs
index ee265c0fe..82fd1cb86 100644
--- a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs
+++ b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs
@@ -163,6 +163,22 @@ public interface IWireMockAdminApi
[Header("Content-Type", "application/json")]
Task PutMappingAsync([Path] Guid guid, [Body] MappingModel mapping, CancellationToken cancellationToken = default);
+ ///
+ /// Enable a mapping based on the guid.
+ ///
+ /// The Guid.
+ /// The optional cancellationToken.
+ [Put("mappings/{guid}/enable")]
+ Task EnableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
+
+ ///
+ /// Disable a mapping based on the guid.
+ ///
+ /// The Guid.
+ /// The optional cancellationToken.
+ [Put("mappings/{guid}/disable")]
+ Task DisableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
+
///
/// Delete a mapping based on the guid
///
diff --git a/src/WireMock.Net.Shared/IMapping.cs b/src/WireMock.Net.Shared/IMapping.cs
index 498638497..ee2f3dacf 100644
--- a/src/WireMock.Net.Shared/IMapping.cs
+++ b/src/WireMock.Net.Shared/IMapping.cs
@@ -108,6 +108,14 @@ public interface IMapping
///
bool IsProxy { get; }
+ ///
+ /// Gets a value indicating whether this mapping is disabled.
+ ///
+ ///
+ /// true if this mapping is disabled; otherwise, false.
+ ///
+ bool IsDisabled { get; set; }
+
///
/// Gets a value indicating whether this mapping to be logged.
///
@@ -135,7 +143,7 @@ public interface IMapping
///
object? Data { get; }
- ///
+ ///
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
///
double? Probability { get; }
diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IsDisabled.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IsDisabled.cs
new file mode 100644
index 000000000..4d247d303
--- /dev/null
+++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IsDisabled.cs
@@ -0,0 +1,134 @@
+// Copyright © WireMock.Net
+
+using RestEase;
+using WireMock.Admin.Mappings;
+using WireMock.Client;
+using WireMock.Server;
+
+namespace WireMock.Net.Tests.AdminApi;
+
+public partial class WireMockAdminApiTests
+{
+ [Fact]
+ public async Task IWireMockAdminApi_PostMappingAsync_WithIsDisabledTrue_DoesNotMatchRequests()
+ {
+ // Arrange
+ var ct = TestContext.Current.CancellationToken;
+ using var server = WireMockServer.StartWithAdminInterface();
+ var api = RestClient.For(server.Urls[0]);
+ var httpClient = server.CreateClient();
+
+ var model = new MappingModel
+ {
+ Request = new RequestModel { Path = "/foo", Methods = ["GET"] },
+ Response = new ResponseModel { Body = "hello", StatusCode = 200 },
+ IsDisabled = true
+ };
+
+ // Act — POST the disabled mapping
+ var postResult = await api.PostMappingAsync(model, ct);
+ postResult.Should().NotBeNull();
+
+ // Assert — request should not be matched (404)
+ var response = await httpClient.GetAsync("/foo", ct);
+ ((int)response.StatusCode).Should().Be(404);
+
+ // Assert — mapping exists but IsDisabled is true
+ server.Mappings.Where(m => !m.IsAdminInterface).Should().ContainSingle(m => m.IsDisabled == true);
+ }
+
+ [Fact]
+ public async Task IWireMockAdminApi_DisableMappingAsync_PreventsMatching()
+ {
+ // Arrange
+ var ct = TestContext.Current.CancellationToken;
+ using var server = WireMockServer.StartWithAdminInterface();
+ var api = RestClient.For(server.Urls[0]);
+ var httpClient = server.CreateClient();
+
+ var model = new MappingModel
+ {
+ Request = new RequestModel { Path = "/bar", Methods = ["GET"] },
+ Response = new ResponseModel { Body = "world", StatusCode = 200 }
+ };
+ var postResult = await api.PostMappingAsync(model, ct);
+ var guid = postResult.Guid!.Value;
+
+ // Assert — mapping matches before disable
+ var before = await httpClient.GetAsync("/bar", ct);
+ ((int)before.StatusCode).Should().Be(200);
+
+ // Act — disable
+ var disableResult = await api.DisableMappingAsync(guid, ct);
+ disableResult.Status.Should().Be("Mapping disabled");
+
+ // Assert — no match after disable
+ var after = await httpClient.GetAsync("/bar", ct);
+ ((int)after.StatusCode).Should().Be(404);
+ }
+
+ [Fact]
+ public async Task IWireMockAdminApi_EnableMappingAsync_ResumesMatching()
+ {
+ // Arrange
+ var ct = TestContext.Current.CancellationToken;
+ using var server = WireMockServer.StartWithAdminInterface();
+ var api = RestClient.For(server.Urls[0]);
+ var httpClient = server.CreateClient();
+
+ var model = new MappingModel
+ {
+ Request = new RequestModel { Path = "/baz", Methods = ["GET"] },
+ Response = new ResponseModel { Body = "re-enabled", StatusCode = 200 },
+ IsDisabled = true
+ };
+ var postResult = await api.PostMappingAsync(model, ct);
+ var guid = postResult.Guid!.Value;
+
+ // Assert — no match while disabled
+ var before = await httpClient.GetAsync("/baz", ct);
+ ((int)before.StatusCode).Should().Be(404);
+
+ // Act — enable
+ var enableResult = await api.EnableMappingAsync(guid, ct);
+ enableResult.Status.Should().Be("Mapping enabled");
+
+ // Assert — mapping matches after enable
+ var after = await httpClient.GetAsync("/baz", ct);
+ ((int)after.StatusCode).Should().Be(200);
+ }
+
+ [Fact]
+ public async Task IWireMockAdminApi_GetMappingAsync_ReturnsIsDisabledTrue_WhenDisabled()
+ {
+ // Arrange
+ var ct = TestContext.Current.CancellationToken;
+ using var server = WireMockServer.StartWithAdminInterface();
+ var api = RestClient.For(server.Urls[0]);
+
+ var disabledModel = new MappingModel
+ {
+ Request = new RequestModel { Path = "/check-disabled" },
+ Response = new ResponseModel { Body = "x", StatusCode = 200 },
+ IsDisabled = true
+ };
+ var enabledModel = new MappingModel
+ {
+ Request = new RequestModel { Path = "/check-enabled" },
+ Response = new ResponseModel { Body = "y", StatusCode = 200 }
+ };
+
+ var disabledPost = await api.PostMappingAsync(disabledModel, ct);
+ var enabledPost = await api.PostMappingAsync(enabledModel, ct);
+
+ // Act
+ var disabledGot = await api.GetMappingAsync(disabledPost.Guid!.Value, ct);
+ var enabledGot = await api.GetMappingAsync(enabledPost.Guid!.Value, ct);
+
+ // Assert — disabled mapping serializes IsDisabled = true
+ disabledGot.IsDisabled.Should().BeTrue();
+
+ // Assert — enabled mapping omits IsDisabled (null = default not disabled)
+ enabledGot.IsDisabled.Should().BeNull();
+ }
+}
diff --git a/test/WireMock.Net.Tests/Constants.cs b/test/WireMock.Net.Tests/Constants.cs
index 30e4cdc7c..aadd6fb9e 100644
--- a/test/WireMock.Net.Tests/Constants.cs
+++ b/test/WireMock.Net.Tests/Constants.cs
@@ -6,5 +6,5 @@ internal static class Constants
{
internal const int NumStaticMappings = 10;
- internal const int NumAdminMappings = 37;
+ internal const int NumAdminMappings = 39;
}
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs
index 945d10d23..17da566c3 100644
--- a/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs
+++ b/test/WireMock.Net.Tests/Owin/MappingMatcherTests.cs
@@ -56,6 +56,7 @@ public void MappingMatcher_FindBestMatch_WhenMappingThrowsException_ShouldReturn
{
// Assign
var mappingMock = new Mock();
+ mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Throws();
var mappings = new ConcurrentDictionary();
@@ -229,6 +230,35 @@ public void MappingMatcher_FindBestMatch_WhenProbabilityDoesMatch_ShouldReturnPr
result.Match!.Mapping.Guid.Should().Be(withProbability);
}
+ [Fact]
+ public void MappingMatcher_FindBestMatch_WhenMappingIsDisabled_ShouldReturnNull()
+ {
+ // Assign
+ var guid = Guid.Parse("00000000-0000-0000-0000-000000000001");
+ var mappingMock = new Mock();
+ mappingMock.SetupGet(m => m.Guid).Returns(guid);
+ mappingMock.SetupGet(m => m.IsDisabled).Returns(true);
+ mappingMock.SetupGet(m => m.Probability).Returns((double?)null);
+
+ var matchResult = new RequestMatchResult();
+ matchResult.AddScore(typeof(object), 1.0, null);
+ mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny())).Returns(matchResult);
+
+ var mappings = new ConcurrentDictionary();
+ mappings.TryAdd(guid, mappingMock.Object);
+ _optionsMock.Setup(o => o.Mappings).Returns(mappings);
+
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
+
+ // Act
+ var result = _sut.FindBestMatch(request);
+
+ // Assert
+ result.Match.Should().BeNull();
+ result.Partial.Should().BeNull();
+ mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny(), It.IsAny()), Times.Never);
+ }
+
private static ConcurrentDictionary InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
{
var mappings = new ConcurrentDictionary();
@@ -237,6 +267,7 @@ private static ConcurrentDictionary InitMappings(params (Guid gu
{
var mappingMock = new Mock();
mappingMock.SetupGet(m => m.Guid).Returns(match.guid);
+ mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
var requestMatchResult = new RequestMatchResult();
foreach (var score in match.scores)