diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln
index a1000602..0438f830 100644
--- a/WireMock.Net Solution.sln
+++ b/WireMock.Net Solution.sln
@@ -128,6 +128,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TestWebApplica
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tests", "test\WireMock.Net.Middleware.Tests\WireMock.Net.Middleware.Tests.csproj", "{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.AwesomeAssertions", "src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj", "{7753670F-7C7F-44BF-8BC7-08325588E60C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -302,6 +304,10 @@ Global
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -351,6 +357,7 @@ Global
{B6269AAC-170A-4346-8B9A-579DED3D9A13} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A}
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A}
+ {7753670F-7C7F-44BF-8BC7-08325588E60C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs
new file mode 100644
index 00000000..65dc2713
--- /dev/null
+++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs
@@ -0,0 +1,40 @@
+// Copyright © WireMock.Net
+
+using Stef.Validation;
+using WireMock.Server;
+
+// ReSharper disable once CheckNamespace
+namespace WireMock.FluentAssertions;
+
+///
+/// Provides assertion methods to verify the number of calls made to a WireMock server.
+/// This class is used in the context of FluentAssertions.
+///
+public class WireMockANumberOfCallsAssertions
+{
+ private readonly IWireMockServer _server;
+ private readonly int _callsCount;
+ private readonly AssertionChain _chain;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The WireMock server to assert against.
+ /// The expected number of calls to assert.
+ /// The assertion chain
+ public WireMockANumberOfCallsAssertions(IWireMockServer server, int callsCount, AssertionChain chain)
+ {
+ _server = Guard.NotNull(server);
+ _callsCount = callsCount;
+ _chain = chain;
+ }
+
+ ///
+ /// Returns an instance of which can be used to assert the expected number of calls.
+ ///
+ /// A instance for asserting the number of calls to the server.
+ public WireMockAssertions Calls()
+ {
+ return new WireMockAssertions(_server, _callsCount, _chain);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs
new file mode 100644
index 00000000..87d8fb0f
--- /dev/null
+++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs
@@ -0,0 +1,83 @@
+// Copyright © WireMock.Net
+
+using WireMock.Extensions;
+using WireMock.Matchers;
+
+// ReSharper disable once CheckNamespace
+namespace WireMock.FluentAssertions;
+
+#pragma warning disable CS1591
+public partial class WireMockAssertions
+{
+ [CustomAssertion]
+ public AndWhichConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs)
+ {
+ _ = AtAbsoluteUrl(new ExactMatcher(true, absoluteUrl), because, becauseArgs);
+
+ return new AndWhichConstraint(this, absoluteUrl);
+ }
+
+ [CustomAssertion]
+ public AndWhichConstraint AtAbsoluteUrl(IStringMatcher absoluteUrlMatcher, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(request => absoluteUrlMatcher.IsPerfectMatch(request.AbsoluteUrl));
+
+ var absoluteUrl = absoluteUrlMatcher.GetPatterns().FirstOrDefault().GetPattern();
+
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.",
+ absoluteUrl
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
+ _ => absoluteUrl,
+ requests => requests.Select(request => request.AbsoluteUrl)
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndWhichConstraint(this, absoluteUrlMatcher);
+ }
+
+ [CustomAssertion]
+ public AndWhichConstraint AtUrl(string url, string because = "", params object[] becauseArgs)
+ {
+ _ = AtUrl(new ExactMatcher(true, url), because, becauseArgs);
+
+ return new AndWhichConstraint(this, url);
+ }
+
+ [CustomAssertion]
+ public AndWhichConstraint AtUrl(IStringMatcher urlMatcher, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(request => urlMatcher.IsPerfectMatch(request.Url));
+
+ var url = urlMatcher.GetPatterns().FirstOrDefault().GetPattern();
+
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.",
+ url
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.",
+ _ => url,
+ requests => requests.Select(request => request.Url)
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndWhichConstraint(this, urlMatcher);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs
new file mode 100644
index 00000000..9c0977ed
--- /dev/null
+++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs
@@ -0,0 +1,35 @@
+// Copyright © WireMock.Net
+
+#pragma warning disable CS1591
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace WireMock.FluentAssertions;
+
+public partial class WireMockAssertions
+{
+ [CustomAssertion]
+ public AndWhichConstraint FromClientIP(string clientIP, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase));
+
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.",
+ clientIP
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.",
+ _ => clientIP, requests => requests.Select(request => request.ClientIP)
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndWhichConstraint(this, clientIP);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs
new file mode 100644
index 00000000..62301d31
--- /dev/null
+++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs
@@ -0,0 +1,81 @@
+// Copyright © WireMock.Net
+
+#pragma warning disable CS1591
+using System;
+using WireMock.Constants;
+
+// ReSharper disable once CheckNamespace
+namespace WireMock.FluentAssertions;
+
+public partial class WireMockAssertions
+{
+ [CustomAssertion]
+ public AndConstraint UsingConnect(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.CONNECT, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingDelete(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.DELETE, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingGet(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.GET, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingHead(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.HEAD, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingOptions(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.OPTIONS, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingPost(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.POST, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingPatch(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.PATCH, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingPut(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.PUT, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingTrace(string because = "", params object[] becauseArgs)
+ => UsingMethod(HttpRequestMethod.TRACE, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingAnyMethod(string because = "", params object[] becauseArgs)
+ => UsingMethod(Any, because, becauseArgs);
+
+ [CustomAssertion]
+ public AndConstraint UsingMethod(string method, string because = "", params object[] becauseArgs)
+ {
+ var any = method == Any;
+ Func predicate = request => (any && !string.IsNullOrEmpty(request.Method)) ||
+ string.Equals(request.Method, method, StringComparison.OrdinalIgnoreCase);
+
+ var (filter, condition) = BuildFilterAndCondition(predicate);
+
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called using method {0}{reason}, but no calls were made.",
+ method
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ "Expected {context:wiremockserver} to have been called using method {0}{reason}, but didn't find it among the methods {1}.",
+ _ => method,
+ requests => requests.Select(request => request.Method)
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndConstraint(this);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs
new file mode 100644
index 00000000..bdd04a42
--- /dev/null
+++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs
@@ -0,0 +1,147 @@
+// Copyright © WireMock.Net
+
+#pragma warning disable CS1591
+using System;
+using System.Collections.Generic;
+using AnyOfTypes;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using WireMock.Extensions;
+using WireMock.Matchers;
+using WireMock.Models;
+
+// ReSharper disable once CheckNamespace
+namespace WireMock.FluentAssertions;
+
+public partial class WireMockAssertions
+{
+ private const string MessageFormatNoCalls = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but no calls were made.";
+ private const string MessageFormat = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but didn't find it among the body/bodies {1}.";
+
+ [CustomAssertion]
+ public AndConstraint WithBody(string body, string because = "", params object[] becauseArgs)
+ {
+ return WithBody(new WildcardMatcher(body), because, becauseArgs);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBody(IStringMatcher matcher, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(r => r.Body, matcher);
+
+ return ExecuteAssertionWithBodyStringMatcher(matcher, because, becauseArgs, condition, filter, r => r.Body);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBodyAsJson(object body, string because = "", params object[] becauseArgs)
+ {
+ return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBodyAsJson(string body, string because = "", params object[] becauseArgs)
+ {
+ return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher);
+
+ return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBodyAsBytes(byte[] body, string because = "", params object[] becauseArgs)
+ {
+ return WithBodyAsBytes(new ExactObjectMatcher(body), because, becauseArgs);
+ }
+
+ [CustomAssertion]
+ public AndConstraint WithBodyAsBytes(ExactObjectMatcher matcher, string because = "", params object[] becauseArgs)
+ {
+ var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsBytes, matcher);
+
+ return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsBytes);
+ }
+
+ private AndConstraint ExecuteAssertionWithBodyStringMatcher(
+ IStringMatcher matcher,
+ string because,
+ object[] becauseArgs,
+ Func, bool> condition,
+ Func, IReadOnlyList> filter,
+ Func expression
+ )
+ {
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ MessageFormatNoCalls,
+ FormatBody(matcher.GetPatterns())
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ MessageFormat,
+ _ => FormatBody(matcher.GetPatterns()),
+ requests => FormatBodies(requests.Select(expression))
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndConstraint(this);
+ }
+
+ private AndConstraint ExecuteAssertionWithBodyAsIObjectMatcher(
+ IObjectMatcher matcher,
+ string because,
+ object[] becauseArgs,
+ Func, bool> condition,
+ Func, IReadOnlyList> filter,
+ Func expression
+ )
+ {
+ _chain
+ .BecauseOf(because, becauseArgs)
+ .Given(() => RequestMessages)
+ .ForCondition(requests => CallsCount == 0 || requests.Any())
+ .FailWith(
+ MessageFormatNoCalls,
+ FormatBody(matcher.Value)
+ )
+ .Then
+ .ForCondition(condition)
+ .FailWith(
+ MessageFormat,
+ _ => FormatBody(matcher.Value),
+ requests => FormatBodies(requests.Select(expression))
+ );
+
+ FilterRequestMessages(filter);
+
+ return new AndConstraint(this);
+ }
+
+ private static string? FormatBody(object? body)
+ {
+ return body switch
+ {
+ null => null,
+ string str => str,
+ AnyOf[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())),
+ byte[] bytes => $"byte[{bytes.Length}] {{...}}",
+ JToken jToken => jToken.ToString(Formatting.None),
+ _ => JToken.FromObject(body).ToString(Formatting.None)
+ };
+ }
+
+ private static string? FormatBodies(IEnumerable