Compare commits

...

56 Commits

Author SHA1 Message Date
Stef Heyenrath
197a211a52 TestcontainersTests 2025-12-13 11:48:48 +01:00
Stef Heyenrath
3cfeec6035 1.19.0 2025-12-12 11:16:38 +01:00
Stef Heyenrath
b57d5e7548 WireMockContainerBuilder: allow only docker images named wiremock (#1392) 2025-12-11 22:21:39 +01:00
Stef Heyenrath
36b89afce5 fix CI link in Readme 2025-12-11 11:25:28 +01:00
Stef Heyenrath
e2acac55a4 Update WireMockContainerBuilder (WithImage and WithCustomImage) (#1391)
* Update WireMockContainerBuilder (WithImage and WithCustomImage)

* .
2025-12-11 10:55:31 +01:00
Stef Heyenrath
ceabd27ce0 1.18.0 2025-12-09 18:28:28 +01:00
Stef Heyenrath
f8e2c7ee90 Add WithBodyAsType to RequestMatcher (#1388)
* Add WithBody<T>

* .

* t

* t2
2025-12-08 19:15:14 +01:00
Stef Heyenrath
c25d8f33d2 1.17.0 2025-12-07 10:55:07 +01:00
Stef Heyenrath
6da190e596 Aspire: Add WithProtoDefinition to support proto definition at server level (#1383)
* Add property UseHttp2 to WireMockServerArguments

* .

* additionalUrls

* ok?

* WireMockServerArguments

* fx

* AddProtoDefinition

* ...

* FIX

* Always add the lifecycle hook to support dynamic mappings and proto definitions
2025-12-07 10:50:11 +01:00
Stef Heyenrath
44388ce80d Fix random delay in mapping json file (#1386) 2025-11-25 20:54:06 +01:00
Stef Heyenrath
5e25ca767d Fix BuildId (#1384) 2025-11-23 11:19:39 +01:00
Stef Heyenrath
0cc583a4a3 WireMock.Net.xUnit.v3 (netstandard2.0) 2025-11-18 18:52:07 +01:00
Stef Heyenrath
f9633adac1 1.16.0 2025-11-18 18:45:12 +01:00
Stef Heyenrath
37bad618a3 Add WireMock.Net.xUnit.v3 project (#1380)
* Add WireMock.Net.xUnit.v3 project

* .
2025-11-18 18:42:28 +01:00
Johannes Häggqvist
8e69f36f04 Add WireMockHealthCheck in WireMock.Net.Aspire (#1375)
* Add WireMockHealthCheck

For use with Aspire, to make WaitFor(wiremock) more useful.
Calls /__admin/health and checks the result, as well as checks if mappings using AdminApiMappingBuilder has been submitted to the server.

This created a catch-22 problem where the mappings were not submitted until the health check was healthy, but the health check was not healthy until the mappings were submitted.

To avoid this, the WireMockServerLifecycleHook class has been slightly re-arranged, and is now using the AfterEndpointsAllocatedAsync callback rather than the AfterResourcesCreatedAsync callback. Within which a separate Task is created that waits until the server is ready and submits the mappings.

* Move WireMockMappingState to its own file

* Dispose the cancellation tokens in WireMockServerLifecycleHook
2025-11-17 20:14:42 +01:00
Stef Heyenrath
21601889e0 Check if the path is valid when using WithPath(...) (#1377) 2025-11-08 09:02:00 +01:00
Stef Heyenrath
dfeabf228e WireMock.Net.OpenApiParser : support Examples (#1366) 2025-11-08 07:45:38 +01:00
Stef Heyenrath
1feb0ade70 Fix wiki links (#1373)
* Change all links from wiki to documention website

* .

* doc

* ws
2025-10-26 10:13:58 +01:00
Stef Heyenrath
8b1bd1b21b 1.15.0 2025-10-22 10:58:44 +02:00
Michi
c1b23b615e Support Testcontainers 4.8.0 (#1370) 2025-10-22 10:18:16 +02:00
Stef Heyenrath
5885324dfb Fix WithProbability logic (#1367)
* Fix WithProbability logic

* .

* FIX

* Update src/WireMock.Net.Minimal/Owin/MappingMatcher.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 10:16:59 +02:00
Tom Akehurst
e3f3e0a8f2 Tweaked docs message in README 2025-10-13 20:29:29 +01:00
Tom Akehurst
685d28db0b Added link to new doc site from README.md 2025-10-13 20:23:18 +01:00
Stef Heyenrath
f6c5225fe0 1.14.0 2025-10-06 17:00:19 +02:00
Stef Heyenrath
b9019a2f61 Update ProxyUrlReplaceSettingsModel with TransformTemplate property (#1362)
* Update ProxyUrlReplaceSettingsModel with TransformTemplate property + parse settings correctly

* oldValue nullable

* <Version>1.14.0-preview-01</Version>
2025-10-06 09:16:25 +02:00
Stef Heyenrath
b82dad2563 Add Tls13 (#1363)
* Add Tls13

* fix
2025-10-05 15:51:47 +02:00
Stef Heyenrath
45d4e7077d 1.13.0 2025-09-28 12:44:14 +02:00
Stef Heyenrath
19e95325fa ProxyUrlTransformer (#1361)
* ProxyUrlTransformer

* tests

* Update src/WireMock.Net.Shared/Settings/ProxyUrlReplaceSettings.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-28 12:40:33 +02:00
Stef Heyenrath
371bfdc160 TypeLoader: implement Try methods (#1358)
* TypeLoader: implement Try methods

* fix
2025-08-31 08:48:29 +02:00
Stef Heyenrath
5c5e104f2c 1.12.0 2025-08-30 11:49:20 +02:00
Stef Heyenrath
068fdf33e3 Upgrade Testcontainers to 4.7.0 (#1357)
* Upgrade Testcontainers to 4.7.0

* .
2025-08-30 11:46:52 +02:00
Stef Heyenrath
358590918e 1.11.2 2025-08-27 10:25:31 +02:00
Stef Heyenrath
32afea5d30 Revert JetBrains.Annotations and add System.Text.RegularExpressions to solve CVE (#1355) 2025-08-27 10:04:36 +02:00
Stef Heyenrath
50b3d58a01 1.11.1 2025-08-27 08:47:58 +02:00
Stef Heyenrath
35bf5e94a5 Add System.Net.Http again to solve CVE warning (#1354) 2025-08-27 08:45:24 +02:00
Stef Heyenrath
9fcc9ade10 Add additional try-catch to TypeLoader logic (#1352) 2025-08-27 08:33:01 +02:00
Stef Heyenrath
865bbf2432 WireMock.Net.ProtoBuf: fix version 2025-08-26 15:42:23 +02:00
Stef Heyenrath
560540099e 1.11.0 2025-08-26 07:58:04 +02:00
Stef Heyenrath
e5c4605020 Create WireMock.Net.ProtoBuf project (#1350)
* Create WireMock.Net.ProtoBuf project

* ok

* Update Directory.Build.props

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-26 07:55:02 +02:00
Stef Heyenrath
124d29c86a 1.10.1 2025-08-22 19:58:00 +02:00
Stef Heyenrath
08117e870b Add AtPath and AtAbsolutePath to Assertions projects (#1349)
* Add AtPath and AtAbsolutePath to Assertions projects

* tst
2025-08-22 19:40:59 +02:00
Stef Heyenrath
ddb181cc52 WireMock.Net.Extensions.Routing 2025-08-18 20:47:10 +02:00
Stef Heyenrath
10fdd24fca 1.10.0 2025-08-18 20:22:49 +02:00
Gennadii Saltyshchak
be2ea67b89 Add new package WireMock.Net.Extensions.Routing which provides minimal-API-style routing for WireMock.Net (#1344)
* Add new package WireMock.Net.Extensions.Routing

* Update documentation for WireMock.Net.Extensions.Routing

* Cleanup imports

* Add header to all source files inside WireMock.Net.Extensions.Routing

* Add header to all source files inside WireMock.Net.Extensions.Routing.Tests

* Revert unintended changes

* Remove redundant build configurations

* Remove incorrect links from documentation

* Update nuget package references

* Revert unintended changes

* Migrate to AwesomeAssertions

* Remove redundant project reference

* Adjust formatting

* Migrate to primary constructor

* Refactoring: rename delegate parameter

* Abstract over JSON converter

* Replace WireMock with WireMock.Net in comments

* Move local functions to the bottom of the methods
2025-08-18 19:52:42 +02:00
Stef Heyenrath
60eb519ae2 1.9.1 2025-08-17 10:11:48 +02:00
Stef Heyenrath
22ed94918a Fix generating source code for Scenario and State (#1347)
* Fix generating source code for Scenario and State

* Update src/WireMock.Net.Minimal/Serialization/MappingConverter.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/WireMock.Net.Minimal/Serialization/MappingConverter.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-17 10:06:39 +02:00
Stef Heyenrath
faffc56484 Add TimesInSameState to MappingModel (#1345)
* Add TimesInSameState to MappingModel

* fix tests
2025-08-11 08:46:18 +02:00
Stef Heyenrath
a5558777e2 1.9.0 2025-08-10 19:07:24 +02:00
Stef Heyenrath
6722ca40ba Update feature_request.md 2025-08-10 19:02:24 +02:00
Stef Heyenrath
0597a73e0e Create GraphQL project (#1334)
* Create new project for GraphQL

* ...

* .

* ok?

* Update src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* --

* ...

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-10 19:00:22 +02:00
Stef Heyenrath
0d510cdde8 1.8.18 2025-08-04 18:03:44 +02:00
Stef Heyenrath
52a396beef 1.8.18 2025-08-04 18:03:23 +02:00
Sam Fields
6ccfe68686 Fixes an issue with matching JSON bodies as bytes (#1339)
* Fixes an issue with matching JSON bodies as bytes

* Adding tests for exact object matching

* Simplify the check for byte data
2025-08-02 20:11:13 +02:00
Stef Heyenrath
e400e92452 1.8.17 2025-07-23 09:40:55 +02:00
Stef Heyenrath
7a187dfb78 Add more examples for WithGraphQLSchema (#1335) 2025-07-23 09:34:31 +02:00
Stef Heyenrath
e6ff8776fb Make CSharpCodeMatcher public (#1337) 2025-07-23 09:29:53 +02:00
209 changed files with 6075 additions and 9062 deletions

View File

@@ -8,7 +8,7 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
@@ -17,7 +17,7 @@ A clear and concise description of what you want to happen.
A clear and concise description of any alternative solutions or features you've considered.
**Is your feature request supported by [WireMock (java version)](https://www.wiremock.org)? Please provide details.**
Provide relevant information if requested feature is supported in [Handlebarsjs](https://handlebarsjs.com/) but is missing in our implementation.
Provide relevant information if requested feature is supported but is missing in this implementation.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,3 +1,84 @@
# 1.19.0 (12 December 2025)
- [#1391](https://github.com/wiremock/WireMock.Net/pull/1391) - Update WireMockContainerBuilder (WithImage and WithCustomImage) [feature] contributed by [StefH](https://github.com/StefH)
- [#1392](https://github.com/wiremock/WireMock.Net/pull/1392) - WireMockContainerBuilder: allow all docker images named wiremock [feature] contributed by [StefH](https://github.com/StefH)
- [#1390](https://github.com/wiremock/WireMock.Net/issues/1390) - Unable to build WireMockContainerBuilder with custom image [feature]
# 1.18.0 (09 December 2025)
- [#1388](https://github.com/wiremock/WireMock.Net/pull/1388) - Add WithBodyAsType to RequestMatcher [feature] contributed by [StefH](https://github.com/StefH)
# 1.17.0 (07 December 2025)
- [#1383](https://github.com/wiremock/WireMock.Net/pull/1383) - Aspire: Add WithProtoDefinition to support proto definition at server level [feature] contributed by [StefH](https://github.com/StefH)
- [#1386](https://github.com/wiremock/WireMock.Net/pull/1386) - Fix random delay in mapping json file [bug] contributed by [StefH](https://github.com/StefH)
- [#1274](https://github.com/wiremock/WireMock.Net/issues/1274) - .WithMappings to mount volume is not working for GRPC [bug]
- [#1381](https://github.com/wiremock/WireMock.Net/issues/1381) - Downstream dependencies missing after 1.16.0 release [bug]
- [#1382](https://github.com/wiremock/WireMock.Net/issues/1382) - Does Aspire support enabling HTTP/2? [feature]
- [#1385](https://github.com/wiremock/WireMock.Net/issues/1385) - Do delays and probabilities show in saved static mappings? [bug]
- [#1387](https://github.com/wiremock/WireMock.Net/issues/1387) - Tests failing with TaskCanceledException on Windows Server 2025 Build 7171 [bug]
# 1.16.0 (18 November 2025)
- [#1366](https://github.com/wiremock/WireMock.Net/pull/1366) - WireMock.Net.OpenApiParser : support Examples [feature] contributed by [StefH](https://github.com/StefH)
- [#1375](https://github.com/wiremock/WireMock.Net/pull/1375) - Add WireMockHealthCheck in WireMock.Net.Aspire [feature] contributed by [Zguy](https://github.com/Zguy)
- [#1377](https://github.com/wiremock/WireMock.Net/pull/1377) - Check if the path is valid when using WithPath(...) [feature] contributed by [StefH](https://github.com/StefH)
- [#1380](https://github.com/wiremock/WireMock.Net/pull/1380) - Add WireMock.Net.xUnit.v3 project [feature] contributed by [StefH](https://github.com/StefH)
- [#1364](https://github.com/wiremock/WireMock.Net/issues/1364) - Choosing examples from open api specification for responses. [feature]
- [#1376](https://github.com/wiremock/WireMock.Net/issues/1376) - AdminApiMappingBuilder `WithPath` should add the starting `/` if missing [feature]
- [#1379](https://github.com/wiremock/WireMock.Net/issues/1379) - xUnit v3 [feature]
# 1.15.0 (22 October 2025)
- [#1367](https://github.com/wiremock/WireMock.Net/pull/1367) - Fix WithProbability logic [bug] contributed by [StefH](https://github.com/StefH)
- [#1370](https://github.com/wiremock/WireMock.Net/pull/1370) - Support Testcontainers 4.8.0 [bug] contributed by [MD-V](https://github.com/MD-V)
- [#1126](https://github.com/wiremock/WireMock.Net/issues/1126) - Request matching WithProbability strange behaviour [bug]
# 1.14.0 (06 October 2025)
- [#1362](https://github.com/wiremock/WireMock.Net/pull/1362) - Update ProxyUrlReplaceSettingsModel with TransformTemplate property [feature] contributed by [StefH](https://github.com/StefH)
- [#1363](https://github.com/wiremock/WireMock.Net/pull/1363) - Add Tls13 [bug] contributed by [StefH](https://github.com/StefH)
- [#1342](https://github.com/wiremock/WireMock.Net/issues/1342) - Facing SSL certificate validation error when using .NET 5 and above framework for some http services during recording [bug]
- [#1360](https://github.com/wiremock/WireMock.Net/issues/1360) - ProxyURL path configuration [feature]
# 1.13.0 (28 September 2025)
- [#1358](https://github.com/wiremock/WireMock.Net/pull/1358) - TypeLoader: implement Try methods [feature] contributed by [StefH](https://github.com/StefH)
- [#1361](https://github.com/wiremock/WireMock.Net/pull/1361) - ProxyUrlTransformer [feature] contributed by [StefH](https://github.com/StefH)
# 1.12.0 (30 August 2025)
- [#1357](https://github.com/wiremock/WireMock.Net/pull/1357) - Upgrade Testcontainers to 4.7.0 [feature] contributed by [StefH](https://github.com/StefH)
- [#1356](https://github.com/wiremock/WireMock.Net/issues/1356) - WireMock.Net 1.11.2 is not compatible with TestContainers 4.7.0 [bug]
# 1.11.2 (27 August 2025)
- [#1352](https://github.com/wiremock/WireMock.Net/pull/1352) - Add additional try-catch to TypeLoader logic [feature] contributed by [StefH](https://github.com/StefH)
- [#1354](https://github.com/wiremock/WireMock.Net/pull/1354) - Add System.Net.Http again to solve CVE warning [bug] contributed by [StefH](https://github.com/StefH)
- [#1355](https://github.com/wiremock/WireMock.Net/pull/1355) - Revert JetBrains.Annotations and add System.Text.RegularExpressions t&#8230; [bug] contributed by [StefH](https://github.com/StefH)
- [#1071](https://github.com/wiremock/WireMock.Net/issues/1071) - Split WireMock into multiple nuget packages depending on features [feature]
- [#1351](https://github.com/wiremock/WireMock.Net/issues/1351) - Version 1.11.0 seems to have a dependency to non-existing version of JetBrans.Annotations [bug]
- [#1353](https://github.com/wiremock/WireMock.Net/issues/1353) - 1.11.0: NU1903: Warning As Error: Package 'System.Net.Http' 4.3.0 has a known high severity vulnerability, https://github.com/advisories/GHSA-7jgj-8wvc-jh57 [bug]
# 1.11.0 (26 August 2025)
- [#1350](https://github.com/wiremock/WireMock.Net/pull/1350) - Create WireMock.Net.ProtoBuf project [feature] contributed by [StefH](https://github.com/StefH)
# 1.10.1 (22 August 2025)
- [#1349](https://github.com/wiremock/WireMock.Net/pull/1349) - Add AtPath and AtAbsolutePath to Assertions projects [feature] contributed by [StefH](https://github.com/StefH)
- [#1348](https://github.com/wiremock/WireMock.Net/issues/1348) - Support AtPath and AtAbsolutePath in WireMockAssertions [feature]
# 1.10.0 (18 August 2025)
- [#1344](https://github.com/wiremock/WireMock.Net/pull/1344) - Add new package WireMock.Net.Extensions.Routing which provides minimal-API-style routing for WireMock.Net [feature] contributed by [GennadyGS](https://github.com/GennadyGS)
- [#1340](https://github.com/wiremock/WireMock.Net/issues/1340) - Feature Request: Add minimal-API-style routing as Extension Package [feature]
# 1.9.1 (17 August 2025)
- [#1345](https://github.com/wiremock/WireMock.Net/pull/1345) - Add TimesInSameState to MappingModel [feature] contributed by [StefH](https://github.com/StefH)
- [#1347](https://github.com/wiremock/WireMock.Net/pull/1347) - Fix generating source code for Scenario and State [bug] contributed by [StefH](https://github.com/StefH)
- [#1343](https://github.com/wiremock/WireMock.Net/issues/1343) - MappingModel allows to configure the times for WillSetStateTo [feature]
- [#1346](https://github.com/wiremock/WireMock.Net/issues/1346) - Mapping: generated C# code is missing InScenario and WillSetStateTo [bug]
# 1.9.0 (10 August 2025)
- [#1334](https://github.com/wiremock/WireMock.Net/pull/1334) - Create GraphQL project [feature] contributed by [StefH](https://github.com/StefH)
# 1.8.18 (04 August 2025)
- [#1339](https://github.com/wiremock/WireMock.Net/pull/1339) - Fixes an issue with matching JSON bodies as bytes [bug] contributed by [smfields](https://github.com/smfields)
- [#1338](https://github.com/wiremock/WireMock.Net/issues/1338) - Specifying .WithBody(byte[]) fails to match for JSON bodies [bug]
# 1.8.17 (23 July 2025)
- [#1337](https://github.com/wiremock/WireMock.Net/pull/1337) - Make CSharpCodeMatcher public [bug] contributed by [StefH](https://github.com/StefH)
- [#1336](https://github.com/wiremock/WireMock.Net/issues/1336) - Is CSharpCodeMatcher actually usable? [bug]
# 1.8.16 (19 July 2025)
- [#1332](https://github.com/wiremock/WireMock.Net/pull/1332) - Use correct Handlebars.Net.Helpers.Xslt (2.5.2) [bug] contributed by [StefH](https://github.com/StefH)
- [#1328](https://github.com/wiremock/WireMock.Net/issues/1328) - ReflectionTypeLoadException occurs when running multiple unit tests (Nunit 3) [bug]

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.8.16</VersionPrefix>
<VersionPrefix>1.19.0</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
@@ -58,7 +58,7 @@
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,6 +1,6 @@
rem https://github.com/StefH/GitHubReleaseNotes
SET version=1.8.16
SET version=1.19.0
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%

View File

@@ -15,48 +15,48 @@ Lightweight Http Mocking Server for .NET, inspired by WireMock.org (from the Jav
### :star: Stubbing
A core feature of WireMock.Net is the ability to return predefined HTTP responses for requests matching criteria.
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
See [Stubbing](https://wiremock.org/dotnet/stubbing).
### :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Request Matching](https://wiremock.org/dotnet/request-matching).
### :star: Response Templating
The response which is returned WireMock.Net can be changed using templating. This is described here [Wiki : Response Templating](https://github.com/wiremock/WireMock.Net/wiki/Response-Templating).
The response which is returned WireMock.Net can be changed using templating. This is described here [Response Templating](https://wiremock.org/dotnet/response-templating).
### :star: Admin API Reference
The WireMock admin API provides functionality to define the mappings via a http interface see [Wiki : Admin API Reference](https://github.com/StefH/WireMock.Net/wiki/Admin-API-Reference).
The WireMock admin API provides functionality to define the mappings via a http interface see [Admin API Reference](https://wiremock.org/dotnet/admin-api-reference).
### :star: Using
WireMock.Net can be used in several ways:
#### UnitTesting
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
[UnitTesting](https://wiremock.org/dotnet/using-wiremock-in-unittests).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
See [WireMock.Net.Testcontainers](https://wiremock.org/dotnet/using-wiremock-net-testcontainers/) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
See [WireMock.Net.Aspire](https://wiremock.org/dotnet/using-wiremock-net-Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
#### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).
It's simple to install WireMock.Net as (global) dotnet tool, see [dotnet tool](https://wiremock.org/dotnet/wiremock-as-dotnet-tool).
#### As standalone process / console application
This is quite straight forward to launch a mock server within a console application, see [Wiki : Standalone Process](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-a-standalone-process).
This is quite straight forward to launch a mock server within a console application, see [Standalone Process](https://wiremock.org/dotnet/wiremock-as-a-standalone-process).
#### As a Windows Service
You can also run WireMock.Net as a Windows Service, follow this [WireMock-as-a-Windows-Service](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-Windows-Service).
You can also run WireMock.Net as a Windows Service, follow this [Windows Service](https://wiremock.org/dotnet/wiremock-as-a-windows-service).
#### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://wiremock.org/dotnet/wiremock-as-a-azure-web-app/)
#### In a docker container
There is also a Linux and Windows-Nano container available at [hub.docker.com](https://hub.docker.com/r/sheyenrath).
For more details see also [Docker](https://github.com/wiremock/WireMock.Net-docker).
#### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))
More details on using HTTPS (SSL) can be found here [HTTPS](https://wiremock.org/dotnet/using-https-ssl/)
## :books: Documentation
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/wiremock/WireMock.Net/wiki/What-Is-WireMock.Net).
For more info, see also this documentation page: [What is WireMock.Net](https://wiremock.org/dotnet/what-is-wiremock-net/).

View File

@@ -1,5 +1,6 @@
# 1.8.16 (19 July 2025)
- #1332 Use correct Handlebars.Net.Helpers.Xslt (2.5.2) [bug]
- #1328 ReflectionTypeLoadException occurs when running multiple unit tests (Nunit 3) [bug]
# 1.19.0 (12 December 2025)
- #1391 Update WireMockContainerBuilder (WithImage and WithCustomImage) [feature]
- #1392 WireMockContainerBuilder: allow all docker images named wiremock [feature]
- #1390 Unable to build WireMockContainerBuilder with custom image [feature]
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -1,7 +1,11 @@
# ![Project Icon](./resources/logo_32x32.png) WireMock.Net
A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) which mimics the functionality from the JAVA based [WireMock](http://wiremock.org).
A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) which mimics functionality from the original Java based WireMock.
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/wiremock/WireMock.Net/wiki/What-Is-WireMock.Net).
---
### :books: Full documentation can now be found at [wiremock.org](https://wiremock.org/dotnet)
---
## :star: Key Features
* HTTP response stubbing, matchable on URL/Path, headers, cookies and body content patterns
@@ -29,7 +33,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
| &nbsp;&nbsp;**Issues** | [![GitHub issues](https://img.shields.io/github/issues/WireMock-Net/WireMock.Net.svg)](https://github.com/wiremock/WireMock.Net/issues) |
| | |
| ***Quality*** | &nbsp; |
| &nbsp;&nbsp;**Build Azure** | [![Build Status Azure](https://stef.visualstudio.com/WireMock.Net/_apis/build/status/WireMock.Net)](https://stef.visualstudio.com/WireMock.Net/_build/latest?definitionId=7) |
| &nbsp;&nbsp;**Build Azure** | [![Build Status Azure](https://stef.visualstudio.com/WireMock.Net/_apis/build/status/WireMock.Net)](https://stef.visualstudio.com/WireMock.Net/_build/latest?definitionId=61) |
| &nbsp;&nbsp;**Quality** | [![Sonar Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=alert_status)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net) [![CodeFactor](https://www.codefactor.io/repository/github/wiremock/wiremock.net/badge)](https://www.codefactor.io/repository/github/wiremock/wiremock.net) |
| &nbsp;&nbsp;**Sonar Bugs** | [![Sonar Bugs](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=bugs)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net&resolved=false&types=BUG) [![Sonar Code Smells](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=code_smells)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net&resolved=false&types=CODE_SMELL) |
| &nbsp;&nbsp;**Coverage** | [![Sonar Coverage](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=coverage)](https://sonarcloud.io/component_measures?id=WireMock-Net_WireMock.Net&metric=coverage) [![codecov](https://codecov.io/gh/wiremock/WireMock.Net/branch/master/graph/badge.svg)](https://codecov.io/gh/wiremock/WireMock.Net)|
@@ -37,7 +41,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
### :package: NuGet packages
| | Official | Preview [:information_source:](https://github.com/wiremock/WireMock.Net/wiki/MyGet-preview-versions) |
| | Official | Preview [:information_source:](https://wiremock.org/dotnet/MyGet-preview-versions) |
| - | - | - |
| &nbsp;&nbsp;**WireMock.Net** | [![NuGet Badge WireMock.Net](https://img.shields.io/nuget/v/WireMock.Net)](https://www.nuget.org/packages/WireMock.Net) | [![MyGet Badge WireMock.Net](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net)
| &nbsp;&nbsp;**WireMock.Net.Minimal** 🔺| [![NuGet Badge WireMock.Net.Minimal](https://img.shields.io/nuget/v/WireMock.Net.Minimal)](https://www.nuget.org/packages/WireMock.Net.Minimal) | [![MyGet Badge WireMock.Net](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Minimal?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Minimal)
@@ -49,18 +53,22 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
| &nbsp;&nbsp;**WireMock.Net.AwesomeAssertions** | [![NuGet Badge WireMock.Net.AwesomeAssertions](https://img.shields.io/nuget/v/WireMock.Net.AwesomeAssertions)](https://www.nuget.org/packages/WireMock.Net.AwesomeAssertions) | [![MyGet Badge WireMock.Net.AwesomeAssertions](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.AwesomeAssertions?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.AwesomeAssertions)
| &nbsp;&nbsp;**WireMock.Net.FluentAssertions** | [![NuGet Badge WireMock.Net.FluentAssertions](https://img.shields.io/nuget/v/WireMock.Net.FluentAssertions)](https://www.nuget.org/packages/WireMock.Net.FluentAssertions) | [![MyGet Badge WireMock.Net.FluentAssertions](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.FluentAssertions?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.FluentAssertions)
| &nbsp;&nbsp;**WireMock.Net.xUnit** | [![NuGet Badge WireMock.Net.xUnit](https://img.shields.io/nuget/v/WireMock.Net.xUnit)](https://www.nuget.org/packages/WireMock.Net.xUnit) | [![MyGet Badge WireMock.Net.xUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.xUnit?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit)
| &nbsp;&nbsp;**WireMock.Net.xUnit.v3** | [![NuGet Badge WireMock.Net.xUnit](https://img.shields.io/nuget/v/WireMock.Net.xUnit.v3)](https://www.nuget.org/packages/WireMock.Net.xUnit.v3) | [![MyGet Badge WireMock.Net.xUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.xUnit.v3?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit.v3)
| &nbsp;&nbsp;**WireMock.Net.TUnit** | [![NuGet Badge WireMock.Net.TUnit](https://img.shields.io/nuget/v/WireMock.Net.TUnit)](https://www.nuget.org/packages/WireMock.Net.TUnit) | [![MyGet Badge WireMock.Net.TUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.TUnit?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.TUnit)
| | | |
| &nbsp;&nbsp;**WireMock.Net.Extensions.Routing** | [![NuGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/nuget/v/WireMock.Net.Extensions.Routing)](https://www.nuget.org/packages/WireMock.Net.Extensions.Routing) | [![MyGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Extensions.Routing?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Extensions.Routing)
| &nbsp;&nbsp;**WireMock.Net.Matchers.CSharpCode** | [![NuGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/nuget/v/WireMock.Net.Matchers.CSharpCode)](https://www.nuget.org/packages/WireMock.Net.Matchers.CSharpCode) | [![MyGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.CSharpCode?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.CSharpCode)
| &nbsp;&nbsp;**WireMock.Net.OpenApiParser** | [![NuGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/nuget/v/WireMock.Net.OpenApiParser)](https://www.nuget.org/packages/WireMock.Net.OpenApiParser) | [![MyGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenApiParser?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenApiParser)
| &nbsp;&nbsp;**WireMock.Net.MimePart** | [![NuGet Badge WireMock.Net.MimePart](https://img.shields.io/nuget/v/WireMock.Net.MimePart)](https://www.nuget.org/packages/WireMock.Net.MimePart) | [![MyGet Badge WireMock.Net.MimePart](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.MimePart?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.MimePart)
| &nbsp;&nbsp;**WireMock.Net.GraphQL** | [![NuGet Badge WireMock.Net.GraphQL](https://img.shields.io/nuget/v/WireMock.Net.GraphQL)](https://www.nuget.org/packages/WireMock.Net.GraphQL) | [![MyGet Badge WireMock.Net.GraphQL](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.GraphQL?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.GraphQL)
| &nbsp;&nbsp;**WireMock.Net.ProtoBuf** | [![NuGet Badge WireMock.Net.ProtoBuf](https://img.shields.io/nuget/v/WireMock.Net.ProtoBuf)](https://www.nuget.org/packages/WireMock.Net.ProtoBuf) | [![MyGet Badge WireMock.Net.ProtoBuf](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.ProtoBuf?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.ProtoBuf)
| | | |
| &nbsp;&nbsp;**WireMock.Net.RestClient** | [![NuGet Badge WireMock.Net.RestClient](https://img.shields.io/nuget/v/WireMock.Net.RestClient)](https://www.nuget.org/packages/WireMock.Net.RestClient) | [![MyGet Badge WireMock.Net.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.RestClient)
| &nbsp;&nbsp;**WireMock.Org.RestClient** | [![NuGet Badge WireMock.Org.RestClient](https://img.shields.io/nuget/v/WireMock.Org.RestClient)](https://www.nuget.org/packages/WireMock.Org.RestClient) | [![MyGet Badge WireMock.Org.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Org.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Org.RestClient)
<br />
🔺 **WireMock.Net.Minimal** does not include: **WireMock.Net.MimePart**
🔺 **WireMock.Net.Minimal** does not include *WireMock.Net.MimePart*, *WireMock.Net.GraphQL* and *WireMock.Net.ProtoBuf*.
---
@@ -86,52 +94,55 @@ To still enable this feature, you need to add the `Environment` category to the
---
## :memo: Development
For the supported frameworks and build information, see [this](https://github.com/wiremock/WireMock.Net/wiki/Development-Information) page.
For the supported frameworks and build information, see [this](https://wiremock.org/dotnet/development-information) page.
## :star: Stubbing
A core feature of WireMock.Net is the ability to return predefined HTTP responses for requests matching criteria.
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
See [Stubbing](https://wiremock.org/dotnet/stubbing).
## :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Request Matching](https://wiremock.org/dotnet/request-matching).
## :star: Response Templating
The response which is returned WireMock.Net can be changed using templating. This is described here [Wiki : Response Templating](https://github.com/wiremock/WireMock.Net/wiki/Response-Templating).
The response which is returned WireMock.Net can be changed using templating. This is described here [Response Templating](https://wiremock.org/dotnet/response-templating).
## :star: Admin API Reference
The WireMock admin API provides functionality to define the mappings via a http interface see [Wiki : Admin API Reference](https://github.com/StefH/WireMock.Net/wiki/Admin-API-Reference).
The WireMock admin API provides functionality to define the mappings via a http interface see [Admin API Reference](https://wiremock.org/dotnet/admin-api-reference).
## :star: Using
WireMock.Net can be used in several ways:
### UnitTesting
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
[UnitTesting](https://wiremock.org/dotnet/using-wiremock-in-unittests).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
See [WireMock.Net.Testcontainers](https://wiremock.org/dotnet/using-wiremock-net-testcontainers/) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
See [WireMock.Net.Aspire](https://wiremock.org/dotnet/using-wiremock-net-Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).
It's simple to install WireMock.Net as (global) dotnet tool, see [dotnet tool](https://wiremock.org/dotnet/wiremock-as-dotnet-tool).
### As standalone process / console application
This is quite straight forward to launch a mock server within a console application, see [Wiki : Standalone Process](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-a-standalone-process).
This is quite straight forward to launch a mock server within a console application, see [Standalone Process](https://wiremock.org/dotnet/wiremock-as-a-standalone-process).
### As a Windows Service
You can also run WireMock.Net as a Windows Service, follow this [WireMock-as-a-Windows-Service](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-Windows-Service).
You can also run WireMock.Net as a Windows Service, follow this [Windows Service](https://wiremock.org/dotnet/wiremock-as-a-windows-service).
### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://wiremock.org/dotnet/wiremock-as-a-azure-web-app/)
### In a docker container
There is also a Linux and Windows-Nano container available at [hub.docker.com](https://hub.docker.com/r/sheyenrath).
For more details see also [Docker](https://github.com/wiremock/WireMock.Net-docker).
#### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))
### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [HTTPS](https://wiremock.org/dotnet/using-https-ssl/)
## :books: Documentation
For more info, see also this documentation page: [What is WireMock.Net](https://wiremock.org/dotnet/what-is-wiremock-net/).
---

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31521.260
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F890C6F-9ACC-438D-928A-AD61CDA862F2}"
EndProject
@@ -136,6 +136,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.ConsoleApp.Usi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Tests.UsingNuGet", "test\WireMock.Net.Tests.UsingNuGet\WireMock.Net.Tests.UsingNuGet.csproj", "{BBA332C6-28A9-42E7-9C4D-A0816E52A198}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.GraphQL", "src\WireMock.Net.GraphQL\WireMock.Net.GraphQL.csproj", "{B6269AAC-170A-4346-8B9A-444DED3D9A45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Extensions.Routing.Tests", "test\WireMock.Net.Extensions.Routing.Tests\WireMock.Net.Extensions.Routing.Tests.csproj", "{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Extensions.Routing", "src\WireMock.Net.Extensions.Routing\WireMock.Net.Extensions.Routing.csproj", "{1E874C8F-08A2-493B-8421-619F9A6E9E77}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.ProtoBuf", "src\WireMock.Net.ProtoBuf\WireMock.Net.ProtoBuf.csproj", "{B47413AA-55D3-49A7-896A-17ADBFF72407}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.xUnit.v3", "src\WireMock.Net.xUnit.v3\WireMock.Net.xUnit.v3.csproj", "{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -326,6 +336,26 @@ Global
{BBA332C6-28A9-42E7-9C4D-A0816E52A198}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA332C6-28A9-42E7-9C4D-A0816E52A198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA332C6-28A9-42E7-9C4D-A0816E52A198}.Release|Any CPU.Build.0 = Release|Any CPU
{B6269AAC-170A-4346-8B9A-444DED3D9A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6269AAC-170A-4346-8B9A-444DED3D9A45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6269AAC-170A-4346-8B9A-444DED3D9A45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6269AAC-170A-4346-8B9A-444DED3D9A45}.Release|Any CPU.Build.0 = Release|Any CPU
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C}.Release|Any CPU.Build.0 = Release|Any CPU
{1E874C8F-08A2-493B-8421-619F9A6E9E77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E874C8F-08A2-493B-8421-619F9A6E9E77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E874C8F-08A2-493B-8421-619F9A6E9E77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E874C8F-08A2-493B-8421-619F9A6E9E77}.Release|Any CPU.Build.0 = Release|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Release|Any CPU.Build.0 = Release|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -379,6 +409,11 @@ Global
{BFEF8990-65B3-4274-310F-7355F0B84035} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{1F80A6E6-D146-4E40-9EA8-49DB8494239F} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{BBA332C6-28A9-42E7-9C4D-A0816E52A198} = {0BB8B634-407A-4610-A91F-11586990767A}
{B6269AAC-170A-4346-8B9A-444DED3D9A45} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C} = {0BB8B634-407A-4610-A91F-11586990767A}
{1E874C8F-08A2-493B-8421-619F9A6E9E77} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{B47413AA-55D3-49A7-896A-17ADBFF72407} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}

View File

@@ -144,6 +144,10 @@ jobs:
vmImage: 'windows-2022'
steps:
- script: |
echo "BuildId = $(buildId)"
displayName: 'Print buildId'
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:

View File

@@ -21,4 +21,13 @@
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
</ItemGroup>
</Project>
<ItemGroup>
<None Update="__admin\mappings\*.proto">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="__admin\mappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -4,12 +4,25 @@ var builder = DistributedApplication.CreateBuilder(args);
// IResourceBuilder<ProjectResource> apiService = builder.AddProject<Projects.AspireApp1_ApiService>("apiservice");
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings");
IResourceBuilder<WireMockServerResource> apiService = builder
.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
//IResourceBuilder<WireMockServerResource> apiService1 = builder
// //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
// .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091")
// .AsHttp2Service()
// .WithMappingsPath(mappingsPath)
// .WithReadStaticMappings()
// .WithWatchStaticMappings()
// .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
IResourceBuilder<WireMockServerResource> apiService2 = builder
.AddWireMock("apiservice", async args =>
{
args.WithAdditionalUrls("http://*:8081", "grpc://*:9093");
args.WithProtoDefinition("my-greeter", await File.ReadAllTextAsync(Path.Combine(mappingsPath, "greet.proto")));
})
.AsHttp2Service()
.WithMappingsPath(mappingsPath)
.WithReadStaticMappings()
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
@@ -45,6 +58,7 @@ IResourceBuilder<WireMockServerResource> apiService = builder
builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService);
.WithReference(apiService2)
.WaitFor(apiService2);
builder.Build().Run();
await builder.Build().RunAsync();

View File

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

View File

@@ -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"
}

View File

@@ -132,7 +132,42 @@ namespace WireMock.Net.ConsoleApplication
}
""";
private const string TestSchema =
private const string TestSchemaQueryStudents =
"""
type Query {
students:[Student]
}
type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}
""";
private const string TestSchemaQueryStudentById =
"""
type Query {
studentById(id:ID!):Student
}
type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}
""";
private const string TestSchemaQueryGreeting =
"""
type Query {
greeting:String
}
""";
private const string TestSchemaMutationMessage =
"""
scalar DateTime
scalar MyCustomScalar
@@ -153,19 +188,6 @@ namespace WireMock.Net.ConsoleApplication
createAnotherMessage(x: MyCustomScalar, dt: DateTime): Message
updateMessage(id: ID!, input: MessageInput): Message
}
type Query {
greeting:String
students:[Student]
studentById(id:ID!):Student
}
type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}
""";
private static void RunSse()
@@ -245,7 +267,7 @@ namespace WireMock.Net.ConsoleApplication
}
}
public static void Run()
public static async Task RunAsync()
{
//RunSse();
//RunOnLocal();
@@ -268,25 +290,56 @@ namespace WireMock.Net.ConsoleApplication
var server = WireMockServer.Start();
//server
// .Given(Request.Create()
// .WithPath("todos")
// .UsingGet()
// )
// .RespondWith(Response.Create()
// .WithBodyAsJson(todos.Values)
// );
//server
// .Given(Request.Create()
// .UsingGet()
// .WithPath("todos")
// .WithParam("id")
// )
// .RespondWith(Response.Create()
// .WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())])
// );
var pX = 0.80;
server
.Given(Request.Create()
.WithPath("todos")
.UsingGet()
)
.RespondWith(Response.Create()
.WithBodyAsJson(todos.Values)
);
.Given(Request.Create().UsingGet().WithPath("/p"))
.WithProbability(pX)
.RespondWith(Response.Create().WithStatusCode(200).WithBody("X"));
server
.Given(Request.Create()
.UsingGet()
.WithPath("todos")
.WithParam("id")
)
.RespondWith(Response.Create()
.WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())])
);
.Given(Request.Create().UsingGet().WithPath("/p"))
.RespondWith(Response.Create().WithStatusCode(200).WithBody("default"));
// Act
var requestUri = new Uri($"http://localhost:{server.Port}/p");
var c = server.CreateClient();
var xCount = 0;
var defaultCount = 0;
var tot = 1000;
for (var i = 0; i < tot; i++)
{
var response = await c.GetAsync(requestUri);
var value = await response.Content.ReadAsStringAsync();
if (value == "X")
{
xCount++;
}
else if (value == "default")
{
defaultCount++;
}
}
System.Console.WriteLine("X = {0} ; default = {1} ; pX = {2:0.00} ; valueX = {3:0.00}", xCount, defaultCount, pX, 1.0 * xCount / tot);
return;
using var httpAndHttpsWithPort = WireMockServer.Start(new WireMockServerSettings
{
HostingScheme = HostingScheme.HttpAndHttps,
@@ -433,10 +486,73 @@ namespace WireMock.Net.ConsoleApplication
.Given(Request.Create()
.WithPath("/graphql")
.UsingPost()
.WithBodyAsGraphQL(TestSchema, customScalars)
.WithGraphQLSchema(TestSchemaQueryStudents)
)
.RespondWith(Response.Create()
.WithBody("GraphQL is ok")
.WithHeader("Content-Type", "application/json")
.WithBody(
"""
{
"data": {
"students": [
{
"id": "1",
"firstName": "Alice",
"lastName": "Johnson",
"fullName": "Alice Johnson"
},
{
"id": "2",
"firstName": "Bob",
"lastName": "Smith",
"fullName": "Bob Smith"
}
]
}
}
""")
);
server
.Given(Request.Create()
.WithPath("/graphql")
.UsingPost()
.WithGraphQLSchema(TestSchemaQueryStudentById)
.WithBody(new JsonPartialWildcardMatcher("{ \"variables\": { \"sid\": \"1\" } }"))
)
.WithTitle("Student found")
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/json")
.WithBody(
"""
{
"data": {
"studentById": {
"id": "123",
"firstName": "John",
"lastName": "Doe",
"fullName": "John Doe"
}
}
}
""")
);
server
.Given(Request.Create()
.WithPath("/graphql")
.UsingPost()
.WithGraphQLSchema(TestSchemaQueryStudentById)
)
.WithTitle("Student not found")
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/json")
.WithBody(
"""
{
"data": null
}
""")
);
#endif

View File

@@ -2,6 +2,7 @@
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using log4net;
using log4net.Config;
using log4net.Repository;
@@ -14,10 +15,10 @@ static class Program
private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
static void Main(params string[] args)
static async Task Main(params string[] args)
{
XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config"));
MainApp.Run();
await MainApp.RunAsync();
}
}

View File

@@ -56,6 +56,11 @@ public class MappingModel
/// In case the value is null state will not be changed.
/// </summary>
public string? SetStateTo { get; set; }
/// <summary>
/// The number of times this match should be matched before the state will be changed to the specified one.
/// </summary>
public int? TimesInSameState { get; set; }
/// <summary>
/// The request model.
@@ -86,7 +91,7 @@ public class MappingModel
/// Fire and forget for webhooks.
/// </summary>
public bool? UseWebhooksFireAndForget { get; set; }
/// <summary>
/// Data Object which can be used when WithTransformer is used.
/// e.g. lookup a path in this object using

View File

@@ -22,7 +22,7 @@ public class MatcherModel
public object? Pattern { get; set; }
/// <summary>
/// Gets or sets the patterns. Can be array of strings (default) or an array of objects.
/// Gets or sets the patterns. Can be an array of strings (default) or an array of objects.
/// </summary>
public object[]? Patterns { get; set; }

View File

@@ -24,4 +24,13 @@ public class StatusModel
/// The error message.
/// </summary>
public string? Error { get; set; }
/// <summary>
/// Returns a string that represents the current status model, including its unique identifier, status, and error information.
/// </summary>
/// <returns>A string containing the values of the Guid, Status, and Error properties formatted for display.</returns>
public override string ToString()
{
return $"StatusModel [Guid={Guid}, Status={Status}, Error={Error}]";
}
}

View File

@@ -1,5 +1,7 @@
// Copyright © WireMock.Net
using WireMock.Types;
namespace WireMock.Admin.Settings;
/// <summary>
@@ -11,15 +13,25 @@ public class ProxyUrlReplaceSettingsModel
/// <summary>
/// The old path value to be replaced by the new path value
/// </summary>
public string OldValue { get; set; } = null!;
public string? OldValue { get; set; }
/// <summary>
/// The new path value to replace the old value with
/// </summary>
public string NewValue { get; set; } = null!;
public string? NewValue { get; set; }
/// <summary>
/// Defines if the case should be ignore when replacing.
/// Defines if the case should be ignored when replacing.
/// </summary>
public bool IgnoreCase { get; set; }
/// <summary>
/// Holds the transformation template.
/// </summary>
public string? TransformTemplate { get; set; }
/// <summary>
/// The transformer type.
/// </summary>
public TransformerType TransformerType { get; set; } = TransformerType.Handlebars;
}

View File

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net
using System;
using WireMock.Validators;
// ReSharper disable once CheckNamespace
namespace WireMock.Admin.Mappings;
@@ -94,9 +95,14 @@ public partial class RequestModelBuilder
}
/// <summary>
/// Set the Path.
/// Set the Path. Must start with a forward slash (/).
/// </summary>
public RequestModelBuilder WithPath(string value) => WithPath(() => value);
public RequestModelBuilder WithPath(string value)
{
PathValidator.ValidateAndThrow(value);
return WithPath(() => value);
}
/// <summary>
/// Set the Path.

View File

@@ -15,12 +15,12 @@ public interface IResponseMessage
/// <summary>
/// The Body.
/// </summary>
IBodyData? BodyData { get; }
IBodyData? BodyData { get; set; }
/// <summary>
/// Gets the body destination (Null, SameAsSource, String or Bytes).
/// </summary>
string? BodyDestination { get; }
string? BodyDestination { get; set; }
/// <summary>
/// Gets or sets the body.
@@ -30,27 +30,27 @@ public interface IResponseMessage
/// <summary>
/// Gets the Fault percentage.
/// </summary>
double? FaultPercentage { get; }
double? FaultPercentage { get; set; }
/// <summary>
/// The FaultType.
/// </summary>
FaultType FaultType { get; }
FaultType FaultType { get; set; }
/// <summary>
/// Gets the headers.
/// </summary>
IDictionary<string, WireMockList<string>>? Headers { get; }
IDictionary<string, WireMockList<string>>? Headers { get; set; }
/// <summary>
/// Gets the trailing headers.
/// </summary>
IDictionary<string, WireMockList<string>>? TrailingHeaders { get; }
IDictionary<string, WireMockList<string>>? TrailingHeaders { get; set; }
/// <summary>
/// Gets or sets the status code.
/// </summary>
object? StatusCode { get; }
object? StatusCode { get; set; }
/// <summary>
/// Adds the header.

View File

@@ -0,0 +1,5 @@
// Copyright © WireMock.Net
namespace WireMock.Models.GraphQL;
public interface ISchemaData;

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using System;
namespace WireMock.Validators;
public static class PathValidator
{
/// <summary>
/// A valid path must start with a '/' and cannot be null, empty or whitespace.
/// </summary>
public static void ValidateAndThrow(string? path, string? paramName = null)
{
if (string.IsNullOrWhiteSpace(path) || path?.StartsWith("/") == false)
{
throw new ArgumentException("Path must start with a '/' and cannot be null, empty or whitespace.", paramName ?? nameof(path));
}
}
}

View File

@@ -38,7 +38,7 @@
</PropertyGroup>
<ItemGroup>
<!-- CVE-2018-8292 -->
<!-- CVE-2018-8292 / https://github.com/advisories/GHSA-7jgj-8wvc-jh57 -->
<PackageReference Include="System.Net.Http " Version="4.3.4" />
<!-- See also https://mstack.nl/blog/20210801-source-generators -->
@@ -50,34 +50,15 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
<PackageReference Include="AnyOf" Version="0.4.0" />-->
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('netstandard')) and '$(TargetFramework)' != 'netstandard1.0'">
<PackageReference Include="System.Security.Cryptography.X509Certificates" Version="4.3.0" />
</ItemGroup>
<!--<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>-->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.3' or '$(TargetFramework)' == 'net45' or '$(TargetFramework)' == 'net451' or '$(TargetFramework)' == 'net461'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<!--<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>-->
<!--<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>-->
</ItemGroup>
</Project>

View File

@@ -30,7 +30,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\WireMock.Net.Minimal\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
<Compile Include="..\WireMock.Net.Minimal\Util\EnhancedFileSystemWatcher.cs" Link="Util\EnhancedFileSystemWatcher.cs" />
<Compile Include="..\WireMock.Net.Minimal\Constants\WireMockConstants.cs" Link="Constants\WireMockConstants.cs" />
<Compile Include="..\WireMock.Net.Shared\Constants\RegexConstants.cs" Link="Constants\RegexConstants.cs" />
<Compile Include="..\WireMock.Net.Minimal\Util\PortUtils.cs" Link="Util\PortUtils.cs" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug - Sonar'">

View File

@@ -0,0 +1,44 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using WireMock.Client;
namespace WireMock.Net.Aspire;
/// <summary>
/// WireMockHealthCheck
/// </summary>
public class WireMockHealthCheck(WireMockServerResource resource) : IHealthCheck
{
private const string HealthStatusHealthy = "Healthy";
/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (!await IsHealthyAsync(resource.AdminApi.Value, cancellationToken))
{
return HealthCheckResult.Unhealthy("WireMock.Net is not healthy");
}
if (resource.ApiMappingState == WireMockMappingState.NotSubmitted)
{
return HealthCheckResult.Unhealthy("WireMock.Net has not received mappings");
}
return HealthCheckResult.Healthy();
}
private static async Task<bool> IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken)
{
try
{
var status = await adminApi.GetHealthAsync(cancellationToken);
return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Aspire;
internal enum WireMockMappingState
{
NoMappings = 0,
NotSubmitted = 1,
Submitted = 2
}

View File

@@ -1,7 +1,9 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -21,10 +23,15 @@ public class WireMockServerArguments
private const string DefaultLogger = "WireMockConsoleLogger";
/// <summary>
/// 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.
/// </summary>
public int? HttpPort { get; set; }
public List<int> HttpPorts { get; set; } = [];
/// <summary>
/// Additional Urls on which WireMock listens.
/// </summary>
public List<string> AdditionalUrls { get; set; } = [];
/// <summary>
/// The admin username.
@@ -67,6 +74,42 @@ public class WireMockServerArguments
/// </summary>
public Func<AdminApiMappingBuilder, CancellationToken, Task>? ApiMappingBuilder { get; set; }
/// <summary>
/// Grpc ProtoDefinitions.
/// </summary>
public Dictionary<string, string[]> ProtoDefinitions { get; set; } = [];
/// <summary>
/// Add an additional Urls on which WireMock should listen.
/// </summary>
/// <param name="additionalUrls">The additional urls which the WireMock Server should listen on.</param>
public void WithAdditionalUrls(params string[] additionalUrls)
{
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);
}
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinitions">The ProtoDefinition as text.</param>
public void WithProtoDefinition(string id, params string[] protoDefinitions)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrEmpty(protoDefinitions);
ProtoDefinitions[id] = protoDefinitions;
}
/// <summary>
/// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server.
/// </summary>
@@ -95,6 +138,11 @@ public class WireMockServerArguments
Add(args, "--WatchStaticMappingsInSubdirectories", "true");
}
if (AdditionalUrls.Count > 0)
{
Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}");
}
return args
.SelectMany(k => new[] { k.Key, k.Value })
.ToArray();

View File

@@ -3,10 +3,12 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.WireMock;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -33,9 +35,31 @@ public static class WireMockServerBuilderExtensions
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 =>
{
callback.HttpPort = port;
if (port != null)
{
serverArguments.HttpPorts = [port.Value];
}
});
}
/// <summary>
/// Adds a WireMock.Net Server resource to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="additionalUrls">The additional urls which the WireMock Server should listen on.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, params string[] additionalUrls)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(additionalUrls);
return builder.AddWireMock(name, serverArguments =>
{
serverArguments.WithAdditionalUrls(additionalUrls);
});
}
@@ -53,13 +77,50 @@ public static class WireMockServerBuilderExtensions
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
var healthCheckKey = $"{name}_check";
var healthCheckRegistration = new HealthCheckRegistration(
healthCheckKey,
_ => new WireMockHealthCheck(wireMockContainerResource),
failureStatus: null,
tags: null);
builder.Services.AddHealthChecks().Add(healthCheckRegistration);
var resourceBuilder = builder
.AddResource(wireMockContainerResource)
.WithImage(DefaultLinuxImage)
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
.WithHealthCheck(healthCheckKey)
.WithWireMockInspectorCommand();
if (arguments.HttpPorts.Count == 0)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
}
else if (arguments.HttpPorts.Count == 1)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: arguments.HttpPorts[0], targetPort: WireMockServerArguments.HttpContainerPort);
}
else
{
// Required for the default admin endpoint and health checks
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
var anyIsHttp2 = false;
foreach (var url in arguments.AdditionalUrls)
{
PortUtils.TryExtract(url, out _, out var isHttp2, out var scheme, out _, out var httpPort);
anyIsHttp2 |= isHttp2;
resourceBuilder = resourceBuilder.WithEndpoint(port: httpPort, targetPort: httpPort, scheme: scheme, name: $"{scheme}-{httpPort}");
}
if (anyIsHttp2)
{
resourceBuilder = resourceBuilder.AsHttp2Service();
}
}
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
@@ -73,6 +134,9 @@ public static class WireMockServerBuilderExtensions
}
});
// Always add the lifecycle hook to support dynamic mappings and proto definitions
resourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
return resourceBuilder;
}
@@ -83,7 +147,10 @@ public static class WireMockServerBuilderExtensions
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="callback">A callback that allows for setting the <see cref="WireMockServerArguments"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, Action<WireMockServerArguments> callback)
public static IResourceBuilder<WireMockServerResource> AddWireMock(
this IDistributedApplicationBuilder builder,
string name,
Action<WireMockServerArguments> callback)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
@@ -154,7 +221,7 @@ public static class WireMockServerBuilderExtensions
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
@@ -165,13 +232,27 @@ public static class WireMockServerBuilderExtensions
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, CancellationToken, Task> configure)
{
Guard.NotNull(wiremock);
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;
return wiremock;
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinitions">The ProtoDefinition as text.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithProtoDefinition(this IResourceBuilder<WireMockServerResource> wiremock, string id, params string[] protoDefinitions)
{
Guard.NotNull(wiremock).Resource.Arguments.WithProtoDefinition(id, protoDefinitions);
return wiremock;
}
@@ -183,11 +264,11 @@ public static class WireMockServerBuilderExtensions
/// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
/// </code>
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{WireMockNetResource}"/>.</param>
/// <returns></returns>
public static IResourceBuilder<WireMockServerResource> WithWireMockInspectorCommand(this IResourceBuilder<WireMockServerResource> builder)
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockNetResource}"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWireMockInspectorCommand(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(builder);
Guard.NotNull(wiremock);
CommandOptions commandOptions = new()
{
@@ -197,13 +278,13 @@ public static class WireMockServerBuilderExtensions
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<ExecuteCommandResult> OnRunOpenInspectorCommandAsync(IResourceBuilder<WireMockServerResource> builder)

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using System.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
@@ -10,32 +11,49 @@ internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDist
{
private readonly CancellationTokenSource _shutdownCts = new();
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
private CancellationTokenSource? _linkedCts;
private Task? _mappingTask;
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();
foreach (var wireMockServerResource in wireMockServerResources)
_mappingTask = Task.Run(async () =>
{
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();
var endpoint = wireMockServerResource.GetEndpoint();
if (endpoint.IsAllocated)
foreach (var wireMockServerResource in wireMockServerResources)
{
await wireMockServerResource.WaitForHealthAsync(cts.Token);
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
var endpoint = wireMockServerResource.GetEndpoint();
Debug.Assert(endpoint.IsAllocated);
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);
await wireMockServerResource.CallAddProtoDefinitionsAsync(_linkedCts.Token);
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);
wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
}
}
}, _linkedCts.Token);
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
await _shutdownCts.CancelAsync();
_linkedCts?.Dispose();
_shutdownCts.Dispose();
if (_mappingTask is not null)
{
await _mappingTask;
}
}
}

View File

@@ -5,6 +5,7 @@ using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Net.Aspire;
using WireMock.Util;
// ReSharper disable once CheckNamespace
@@ -19,6 +20,7 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
internal WireMockServerArguments Arguments { get; }
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
internal WireMockMappingState ApiMappingState { get; set; } = WireMockMappingState.NoMappings;
private ILogger? _logger;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
@@ -64,6 +66,36 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
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
{
var status = await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken);
_logger?.LogInformation("ProtoDefinition '{Id}' added with status: {Status}.", id, status.Status);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", id);
}
}
}
// 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)
@@ -109,10 +141,17 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
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);
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)
{

View File

@@ -4,11 +4,11 @@ using Stef.Validation;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
/// <summary>
/// Provides assertion methods to verify the number of calls made to a WireMock server.
/// This class is used in the context of FluentAssertions.
/// This class is used in the context of AwesomeAssertions.
/// </summary>
public class WireMockANumberOfCallsAssertions
{

View File

@@ -0,0 +1,83 @@
// Copyright © WireMock.Net
using WireMock.Extensions;
using WireMock.Matchers;
// ReSharper disable once CheckNamespace
namespace WireMock.AwesomeAssertions;
#pragma warning disable CS1591
public partial class WireMockAssertions
{
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> AtAbsolutePath(string absolutePath, string because = "", params object[] becauseArgs)
{
_ = AtAbsolutePath(new ExactMatcher(true, absolutePath), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, absolutePath);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtAbsolutePath(IStringMatcher absolutePathMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => absolutePathMatcher.IsPerfectMatch(request.AbsolutePath));
var absolutePath = absolutePathMatcher.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 path {0}{reason}, but no calls were made.",
absolutePath
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute path {0}{reason}, but didn't find it among the calls to {1}.",
_ => absolutePath,
requests => requests.Select(request => request.AbsolutePath)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, absolutePathMatcher);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> AtPath(string path, string because = "", params object[] becauseArgs)
{
_ = AtPath(new ExactMatcher(true, path), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, path);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtPath(IStringMatcher pathMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => pathMatcher.IsPerfectMatch(request.Path));
var path = pathMatcher.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 path {0}{reason}, but no calls were made.",
path
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the path {0}{reason}, but didn't find it among the calls to {1}.",
_ => path,
requests => requests.Select(request => request.Path)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, pathMatcher);
}
}

View File

@@ -4,7 +4,7 @@ using WireMock.Extensions;
using WireMock.Matchers;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
#pragma warning disable CS1591
public partial class WireMockAssertions

View File

@@ -4,7 +4,7 @@
using System;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -5,7 +5,7 @@ using System;
using WireMock.Constants;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -11,7 +11,7 @@ using WireMock.Matchers;
using WireMock.Models;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -3,7 +3,7 @@
#pragma warning disable CS1591
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -4,7 +4,7 @@
using System;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -7,7 +7,7 @@ using WireMock.Matchers;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
public partial class WireMockAssertions
{

View File

@@ -4,7 +4,7 @@ using AwesomeAssertions.Primitives;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
namespace WireMock.AwesomeAssertions;
/// <summary>
/// Contains a number of methods to assert that the <see cref="IWireMockServer"/> is in the expected state.

View File

@@ -3,7 +3,7 @@
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions
namespace WireMock.AwesomeAssertions
{
/// <summary>
/// Contains extension methods for custom assertions in unit tests.

View File

@@ -0,0 +1,10 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Extensions.Routing.Delegates;
/// <summary>
/// Represents a handler for processing WireMock.Net HTTP requests and returning a response asynchronously.
/// </summary>
/// <param name="requestMessage">The incoming request message.</param>
/// <returns>A task that resolves to a <see cref="ResponseMessage"/>.</returns>
public delegate Task<IResponseMessage> WireMockHttpRequestHandler(IRequestMessage requestMessage);

View File

@@ -0,0 +1,10 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Extensions.Routing.Delegates;
/// <summary>
/// Represents a middleware component for WireMock.Net HTTP request handling.
/// </summary>
/// <param name="next">The next request handler in the middleware pipeline.</param>
/// <returns>A <see cref="WireMockHttpRequestHandler"/> that processes the request.</returns>
public delegate WireMockHttpRequestHandler WireMockMiddleware(WireMockHttpRequestHandler next);

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using System.Collections.Immutable;
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class DictionaryExtensions
{
public static IDictionary<TKey, TValue> AddIf<TKey, TValue>(
this IDictionary<TKey, TValue> source,
bool condition,
TKey key,
TValue value,
IEqualityComparer<TKey>? keyComparer = null)
where TKey : notnull =>
condition
? source.ToImmutableDictionary(keyComparer).Add(key, value)
: source;
}

View File

@@ -0,0 +1,34 @@
// Copyright © WireMock.Net
using Microsoft.AspNetCore.Http;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class HttpResponseExtensions
{
public static async Task<ResponseMessage> ToResponseMessageAsync(
this HttpResponse response)
{
var headers = response.Headers.ToDictionary(
header => header.Key, header => new WireMockList<string?>(header.Value.ToArray()));
return new()
{
Headers = headers!,
BodyData = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = await response.ReadBodyAsStringAsync(),
},
StatusCode = response.StatusCode,
};
}
public static async Task<string> ReadBodyAsStringAsync(this HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(response.Body);
return await reader.ReadToEndAsync();
}
}

View File

@@ -0,0 +1,16 @@
// Copyright © WireMock.Net
using JsonConverter.Abstractions;
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class RequestMessageExtensions
{
public static T? GetBodyAsJson<T>(
this IRequestMessage requestMessage,
IJsonConverter jsonConverter,
JsonConverterOptions? jsonOptions = null) =>
requestMessage.Body is not null
? jsonConverter.Deserialize<T>(requestMessage.Body, jsonOptions)
: default;
}

View File

@@ -0,0 +1,9 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class StringExtensions
{
public static string ToMatchFullStringRegex(this string regex) =>
$"^{regex}$";
}

View File

@@ -0,0 +1,39 @@
// Copyright © WireMock.Net
using System.Reflection;
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class TaskExtensions
{
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Usage",
"VSTHRD003:Avoid awaiting foreign Tasks",
Justification = "Await is required here to transform base task to generic one.")]
public static async Task<object?> ToGenericTaskAsync(this Task task)
{
await task;
var taskType = task.GetType();
if (!IsAssignableToGenericTaskType(taskType))
{
return null;
}
return task
.GetType()
.GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)!
.GetValue(task);
}
private static bool IsAssignableToGenericTaskType(Type type)
{
if (type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Task<>) &&
type.GetGenericArguments()[0] != Type.GetType("System.Threading.Tasks.VoidTaskResult"))
{
return true;
}
return type.BaseType is not null && IsAssignableToGenericTaskType(type.BaseType);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright © WireMock.Net
using WireMock.Net.Extensions.Routing.Delegates;
namespace WireMock.Net.Extensions.Routing.Extensions;
internal static class WireMockHttpRequestHandlerExtensions
{
public static WireMockHttpRequestHandler UseMiddleware(
this WireMockHttpRequestHandler handler, WireMockMiddleware middleware) =>
middleware(handler);
public static WireMockHttpRequestHandler UseMiddlewareCollection(
this WireMockHttpRequestHandler handler,
IReadOnlyCollection<WireMockMiddleware> middlewareCollection) =>
middlewareCollection.Aggregate(handler, UseMiddleware);
}

View File

@@ -0,0 +1,224 @@
// Copyright © WireMock.Net
using JsonConverter.Abstractions;
using WireMock.Net.Extensions.Routing.Models;
namespace WireMock.Net.Extensions.Routing.Extensions;
/// <summary>
/// Provides extension methods for mapping HTTP routes to handlers in <see cref="WireMockRouter"/>.
/// </summary>
public static class WireMockRouterExtensions
{
/// <summary>
/// Maps a GET request to a synchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapGet(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, object?> requestHandler) =>
source.Map(HttpMethod.Get.Method, pattern, requestHandler);
/// <summary>
/// Maps a GET request to an asynchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapGet(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, Task<object?>> requestHandler) =>
source.Map(HttpMethod.Get.Method, pattern, requestHandler);
/// <summary>
/// Maps a POST request to a synchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPost(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, object?> requestHandler) =>
source.Map(HttpMethod.Post.Method, pattern, requestHandler);
/// <summary>
/// Maps a POST request to an asynchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPost(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, Task<object?>> requestHandler) =>
source.Map(HttpMethod.Post.Method, pattern, requestHandler);
/// <summary>
/// Maps a POST request to a synchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPost<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, object?> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Post.Method, pattern, requestHandler, jsonConverter, jsonOptions);
/// <summary>
/// Maps a POST request to an asynchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPost<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, Task<object?>> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Post.Method, pattern, requestHandler, jsonConverter, jsonOptions);
/// <summary>
/// Maps a PUT request to a synchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPut(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, object?> requestHandler) =>
source.Map(HttpMethod.Put.Method, pattern, requestHandler);
/// <summary>
/// Maps a PUT request to an asynchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPut(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, Task<object?>> requestHandler) =>
source.Map(HttpMethod.Put.Method, pattern, requestHandler);
/// <summary>
/// Maps a PUT request to a synchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPut<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, object?> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Put.Method, pattern, requestHandler, jsonConverter, jsonOptions);
/// <summary>
/// Maps a PUT request to an asynchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapPut<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, Task<object?>> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Put.Method, pattern, requestHandler, jsonConverter, jsonOptions);
/// <summary>
/// Maps a DELETE request to a synchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapDelete(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, object?> requestHandler) =>
source.Map(HttpMethod.Delete.Method, pattern, requestHandler);
/// <summary>
/// Maps a DELETE request to an asynchronous request handler.
/// </summary>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapDelete(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo, Task<object?>> requestHandler) =>
source.Map(HttpMethod.Delete.Method, pattern, requestHandler);
/// <summary>
/// Maps a DELETE request to a synchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapDelete<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, object?> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Delete.Method, pattern, requestHandler, jsonConverter, jsonOptions);
/// <summary>
/// Maps a DELETE request to an asynchronous request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="source">The router to extend.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public static WireMockRouter MapDelete<TRequest>(
this WireMockRouter source,
string pattern,
Func<WireMockRequestInfo<TRequest>, Task<object?>> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null) =>
source.Map(HttpMethod.Delete.Method, pattern, requestHandler, jsonConverter, jsonOptions);
}

View File

@@ -0,0 +1,35 @@
// Copyright © WireMock.Net
using WireMock.Matchers;
using WireMock.Net.Extensions.Routing.Delegates;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
namespace WireMock.Net.Extensions.Routing.Extensions;
/// <summary>
/// Provides extension methods for mapping HTTP requests to handlers in <see cref="WireMockServer"/>.
/// </summary>
public static class WireMockServerExtensions
{
/// <summary>
/// Maps a request to a WireMock.Net server using the specified method, path matcher, and request handler.
/// </summary>
/// <param name="source">The WireMock.Net server to extend.</param>
/// <param name="method">The HTTP method to match.</param>
/// <param name="pathMatcher">The matcher for the request path.</param>
/// <param name="httpRequestHandler">The handler to process the request.</param>
/// <returns>The current <see cref="WireMockServer"/> instance.</returns>
public static WireMockServer Map(
this WireMockServer source,
string method,
IStringMatcher pathMatcher,
WireMockHttpRequestHandler httpRequestHandler)
{
source
.Given(Request.Create().WithPath(pathMatcher).UsingMethod(method))
.RespondWith(Response.Create().WithCallback(req => httpRequestHandler(req)));
return source;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Extensions.Routing.Models;
/// <summary>
/// Represents request information for WireMock.Net routing, including the request message and route arguments.
/// </summary>
public class WireMockRequestInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="WireMockRequestInfo"/> class.
/// </summary>
/// <param name="request">The incoming request message.</param>
public WireMockRequestInfo(IRequestMessage request)
{
Request = request;
}
/// <summary>
/// Gets the incoming request message.
/// </summary>
public IRequestMessage Request { get; }
/// <summary>
/// Gets or initializes the route arguments extracted from the request path.
/// </summary>
public IDictionary<string, object> RouteArgs { get; init; } =
new Dictionary<string, object>();
}

View File

@@ -0,0 +1,24 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Extensions.Routing.Models;
/// <summary>
/// Represents request information with a strongly-typed deserialized body for WireMock.Net routing.
/// </summary>
/// <typeparam name="TBody">The type of the deserialized request body.</typeparam>
public sealed class WireMockRequestInfo<TBody> : WireMockRequestInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="WireMockRequestInfo{TBody}"/> class.
/// </summary>
/// <param name="request">The incoming request message.</param>
public WireMockRequestInfo(IRequestMessage request)
: base(request)
{
}
/// <summary>
/// Gets or initializes the deserialized request body.
/// </summary>
public TBody? Body { get; init; }
}

View File

@@ -0,0 +1,138 @@
# WireMock.Net.Extensions.Routing
**WireMock.Net.Extensions.Routing** extends [WireMock.Net](https://github.com/wiremock/wiremock) with modern, minimal-API-style routing for .NET. It provides extension methods for expressive, maintainable, and testable HTTP routing, inspired by [ASP.NET Core Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-9.0).
---
## Motivation
While [WireMock.Net](https://github.com/WireMock-Net/WireMock.Net) is a powerful tool for HTTP mocking in .NET, its native API for defining routes and request handlers can be verbose and require significant boilerplate. Setting up even simple endpoints often involves multiple chained method calls, manual parsing of request data, and repetitive configuration, which can make tests harder to read and maintain.
**WireMock.Net.Extensions.Routing** addresses these pain points by introducing a concise, fluent, and minimal-API-inspired approach to routing. This makes your test code:
- **More readable:** Route definitions are clear and expressive, closely resembling production minimal APIs.
- **Easier to maintain:** Less boilerplate means fewer places for errors and easier refactoring.
- **Faster to write:** Define routes and handlers in a single line, with strong typing and async support.
### Example: Native WireMock.Net vs. WireMock.Net.Extensions.Routing
#### Native WireMock.Net
```csharp
server.Given(
Request.Create().WithPath("/hello").UsingGet()
)
.RespondWith(
Response.Create().WithBody("Hello, world!")
);
server.Given(
Request.Create().WithPath("/user/*").UsingGet()
)
.RespondWith(
Response.Create().WithCallback(request =>
{
var id = request.PathSegments[1];
// ...fetch user by id...
return new ResponseMessage { Body = $"User: {id}" };
})
);
```
#### With WireMock.Net.Extensions.Routing
```csharp
router.MapGet("/hello", _ => "Hello, world!");
router.MapGet("/user/{id:int}", requestInfo =>
{
var id = requestInfo.RouteArgs["id"];
// ...fetch user by id...
return $"User: {id}";
});
```
With **WireMock.Net.Extensions.Routing**, you get:
- Minimal, one-line route definitions
- Typed route parameters (e.g., `{id:int}`)
- Direct access to parsed route arguments and request bodies
- Async handler support
This leads to more maintainable, scalable, and production-like test code.
---
## Features
- Minimal API-style route definitions for WireMock.Net
- Strongly-typed request handling
- Routing parameters with constraints (`int` and `string` are currently supported)
- Asynchronous handlers
- Fluent, composable routing extensions
- Easy integration with existing WireMock.Net servers
- .NET 8+ support
---
## Installation
Install from NuGet:
```shell
dotnet add package WireMock.Net.Extensions.Routing
```
---
## Quick Start
```csharp
using System.Net.Http.Json;
using WireMock.Net.Extensions.Routing;
using WireMock.Net.Extensions.Routing.Extensions;
using WireMock.Server;
var server = WireMockServer.Start();
var router = new WireMockRouter(server);
router.MapGet("/hello", _ => "Hello, world!");
using var client = server.CreateClient();
var result = await client.GetFromJsonAsync<string>("/hello");
// Hello, world!
```
---
## Usage
### Routing with route parameters
```csharp
router.MapGet("/user/{id:int}", async requestInfo => {
var userId = requestInfo.RouteArgs["id"];
// var user = await ...
return user;
});
```
### Strongly-Typed Request Info
```csharp
router.MapPost<Item>("/api/items", requestInfo => {
var item = requestInfo.Body!;
// process item
return Results.Json(new { success = true });
});
```
### Supported Methods
- `MapGet`, `MapPost`, `MapPut`, `MapDelete`
---
## Documentation
- [API Reference](./src/WireMock.Net.Extensions.Routing/)
- [WireMock.Net Documentation](https://github.com/WireMock-Net/WireMock.Net)

View File

@@ -0,0 +1,107 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.RegularExpressions;
using WireMock.Net.Extensions.Routing.Extensions;
namespace WireMock.Net.Extensions.Routing.Utils;
internal static class RoutePattern
{
private static readonly Regex ArgRegex =
new(@"{(?'name'\w+)(?::(?'type'\w+))?}", RegexOptions.Compiled);
public static IDictionary<string, object> GetArgs(string pattern, string route) =>
TryGetArgs(pattern, route, out var args)
? args
: throw new InvalidOperationException(
$"Url {route} does not match route pattern {pattern}");
public static bool TryGetArgs(
string pattern, string route, [NotNullWhen(true)] out IDictionary<string, object>? args)
{
var regex = new Regex(ToRegex(pattern), RegexOptions.IgnoreCase);
var match = regex.Match(route);
if (!match.Success)
{
args = null;
return false;
}
var routeArgTypeMap = GetArgTypeMap(pattern);
args = match.Groups
.Cast<Group>()
.Where(g => g.Index > 0)
.ToDictionary(g => g.Name, g => routeArgTypeMap[g.Name].Parse(g.Value));
return true;
}
public static string ToRegex(string pattern)
{
return ArgRegex
.Replace(pattern, m => $"(?'{m.Groups["name"].Value}'{GetArgMatchingRegex(m)})")
.ToMatchFullStringRegex();
static string GetArgMatchingRegex(Match match) =>
ArgType.GetByName(match.Groups["type"].Value).GetRegex();
}
private static IDictionary<string, ArgType> GetArgTypeMap(string pattern) =>
ArgRegex
.Matches(pattern)
.ToDictionary(
m => m.Groups["name"].Value, m => ArgType.GetByName(m.Groups["type"].Value));
private abstract record ArgType
{
private ArgType(string name)
{
Name = name;
}
public string Name { get; }
public static ArgType String { get; } = new StringArgType();
public static ArgType Int { get; } = new IntArgType();
private static IReadOnlyCollection<ArgType> All { get; } =
typeof(ArgType)
.GetProperties(BindingFlags.Public | BindingFlags.Static)
.Where(p => p.PropertyType.IsAssignableTo(typeof(ArgType)))
.Select(p => p.GetValue(null))
.Cast<ArgType>()
.ToList();
private static IReadOnlyDictionary<string, ArgType> MapByName { get; } =
All.ToDictionary(x => x.Name);
public static ArgType GetByName(string name) =>
GetByNameOrDefault(name)
?? throw new InvalidOperationException($"Route argument type {name} is not found");
public static ArgType? GetByNameOrDefault(string name) =>
!string.IsNullOrEmpty(name)
? MapByName.GetValueOrDefault(name)
: String;
public abstract object Parse(string input);
public abstract string GetRegex();
private sealed record StringArgType() : ArgType("string")
{
public override object Parse(string input) => input;
public override string GetRegex() => @".*";
}
private sealed record IntArgType() : ArgType("int")
{
public override object Parse(string input) => int.Parse(input);
public override string GetRegex() => @"-?\d+";
}
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>WireMock.Net.Routing extends WireMock.Net with modern, minimal-API-style routing for .NET</Description>
<Authors>Gennadii Saltyshchak</Authors>
<TargetFrameworks>net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>tdd;mock;http;wiremock;test;server;unittest;routing;minimalapi</PackageTags>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageReadmeFile>README.md</PackageReadmeFile>
<ImplicitUsings>enable</ImplicitUsings>
<!--<TreatWarningsAsErrors>true</TreatWarningsAsErrors>-->
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Link="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Minimal\WireMock.Net.Minimal.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,153 @@
// Copyright © WireMock.Net
using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using WireMock.Matchers;
using WireMock.Net.Extensions.Routing.Delegates;
using WireMock.Net.Extensions.Routing.Extensions;
using WireMock.Net.Extensions.Routing.Models;
using WireMock.Net.Extensions.Routing.Utils;
using WireMock.Server;
namespace WireMock.Net.Extensions.Routing;
/// <summary>
/// Provides routing and request mapping functionality for WireMock.Net,
/// mimicking ASP.NET Core Minimal APIs routing style.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="WireMockRouter"/> class.
/// </remarks>
/// <param name="server">The WireMock.Net server instance.</param>
public sealed class WireMockRouter(WireMockServer server)
{
private readonly WireMockServer _server = server;
/// <summary>
/// Gets or initializes the collection of middleware for the router.
/// </summary>
public IReadOnlyCollection<WireMockMiddleware> MiddlewareCollection { get; init; } = [];
/// <summary>
/// Gets or initializes the default <see cref="IJsonConverter"/> [optional].
/// </summary>
public IJsonConverter? DefaultJsonConverter { get; init; }
/// <summary>
/// Gets or initializes the default <see cref="JsonConverterOptions"/> [optional].
/// </summary>
public JsonConverterOptions? DefaultJsonOptions { get; init; }
/// <summary>
/// Maps a route to a synchronous request handler.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public WireMockRouter Map(
string method, string pattern, Func<WireMockRequestInfo, object?> requestHandler)
{
return Map(method, pattern, CreateResponse);
object? CreateResponse(IRequestMessage request) =>
requestHandler(CreateRequestInfo(request, pattern));
}
/// <summary>
/// Maps a route to an asynchronous request handler.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The asynchronous request handler function.</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public WireMockRouter Map(
string method, string pattern, Func<WireMockRequestInfo, Task<object?>> requestHandler)
{
return Map(method, pattern, CreateResponseAsync);
Task<object?> CreateResponseAsync(IRequestMessage request) =>
requestHandler(CreateRequestInfo(request, pattern));
}
/// <summary>
/// Maps a route to a request handler with a typed body.
/// </summary>
/// <typeparam name="TRequest">The type of the request body.</typeparam>
/// <param name="method">The HTTP method.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="requestHandler">The request handler function.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="jsonOptions">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>The current <see cref="WireMockRouter"/> instance.</returns>
public WireMockRouter Map<TRequest>(
string method,
string pattern,
Func<WireMockRequestInfo<TRequest>, object?> requestHandler,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null)
{
return Map(method, pattern, CreateBody);
object? CreateBody(IRequestMessage request) =>
requestHandler(CreateRequestInfo<TRequest>(request, pattern, jsonConverter, jsonOptions));
}
private static WireMockRequestInfo CreateRequestInfo(IRequestMessage request, string pattern) =>
new(request)
{
RouteArgs = RoutePattern.GetArgs(pattern, request.Path),
};
private static WireMockHttpRequestHandler CreateHttpRequestHandler(
Func<IRequestMessage, object?> requestHandler) =>
request => CreateResponseMessageAsync(requestHandler(request));
private static async Task<IResponseMessage> CreateResponseMessageAsync(object? response)
{
var awaitedResponse = response is Task task
? await task.ToGenericTaskAsync()
: response;
var result = awaitedResponse as IResult ?? Results.Ok(awaitedResponse);
var httpContext = CreateHttpContext();
await result.ExecuteAsync(httpContext);
return await httpContext.Response.ToResponseMessageAsync();
}
private static HttpContext CreateHttpContext() =>
new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(),
Response = { Body = new MemoryStream() },
};
private WireMockRequestInfo<TRequest> CreateRequestInfo<TRequest>(
IRequestMessage request,
string pattern,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? jsonOptions = null)
{
var requestInfo = CreateRequestInfo(request, pattern);
var establishedJsonConverter =
jsonConverter ?? DefaultJsonConverter ?? new NewtonsoftJsonConverter();
var establishedJsonOptions = jsonOptions ?? DefaultJsonOptions;
return new WireMockRequestInfo<TRequest>(requestInfo.Request)
{
RouteArgs = requestInfo.RouteArgs,
Body = requestInfo.Request.GetBodyAsJson<TRequest>(
establishedJsonConverter, establishedJsonOptions),
};
}
private WireMockRouter Map(
string method, string pattern, Func<IRequestMessage, object?> requestHandler)
{
var matcher = new RegexMatcher(RoutePattern.ToRegex(pattern), ignoreCase: true);
var httpRequestHandler =
CreateHttpRequestHandler(requestHandler).UseMiddlewareCollection(MiddlewareCollection);
_server.Map(method, matcher, httpRequestHandler);
return this;
}
}

View File

@@ -0,0 +1,73 @@
// Copyright © WireMock.Net
using System.Collections.Concurrent;
using JsonConverter.Abstractions;
using WireMock.Net.Extensions.Routing.Delegates;
using WireMock.Server;
namespace WireMock.Net.Extensions.Routing;
/// <summary>
/// Provides a builder for configuring and creating a <see cref="WireMockRouter"/> with middleware and JSON settings.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="WireMockServerRouterBuilder"/> class.
/// </remarks>
/// <param name="server">The WireMock.Net server instance.</param>
public sealed class WireMockServerRouterBuilder(WireMockServer server)
{
private readonly WireMockServer _server = server;
private readonly ConcurrentQueue<WireMockMiddleware> _middlewareCollection = new();
private IJsonConverter? _defaultJsonConverter;
private JsonConverterOptions? _defaultJsonOptions;
/// <summary>
/// Builds a <see cref="WireMockRouter"/> with the configured middleware and JSON settings.
/// </summary>
/// <returns>The configured <see cref="WireMockRouter"/>.</returns>
public WireMockRouter Build() =>
new(_server)
{
MiddlewareCollection = _middlewareCollection,
DefaultJsonConverter = _defaultJsonConverter,
DefaultJsonOptions = _defaultJsonOptions,
};
/// <summary>
/// Adds a middleware to the router builder.
/// </summary>
/// <param name="middleware">The middleware to add.</param>
/// <returns>The current <see cref="WireMockServerRouterBuilder"/> instance.</returns>
public WireMockServerRouterBuilder Use(WireMockMiddleware middleware)
{
_middlewareCollection.Enqueue(middleware);
return this;
}
/// <summary>
/// Sets the default <see cref="IJsonConverter"/>.
/// </summary>
/// <param name="defaultJsonConverter">the default <see cref="IJsonConverter"/></param>
/// <returns>The current <see cref="WireMockServerRouterBuilder"/> instance.</returns>
public WireMockServerRouterBuilder WithDefaultJsonConverter(
IJsonConverter? defaultJsonConverter)
{
_defaultJsonConverter = defaultJsonConverter;
return this;
}
/// <summary>
/// Sets the default <see cref="JsonConverterOptions"/> [optional].
/// </summary>
/// <param name="defaultJsonOptions">the default <see cref="JsonConverterOptions"/> [optional]</param>
/// <returns>The current <see cref="WireMockServerRouterBuilder"/> instance.</returns>
public WireMockServerRouterBuilder WithDefaultJsonOptions(
JsonConverterOptions? defaultJsonOptions)
{
_defaultJsonOptions = defaultJsonOptions;
return this;
}
}

View File

@@ -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<WireMockAssertions, string> AtAbsolutePath(string absolutePath, string because = "", params object[] becauseArgs)
{
_ = AtAbsolutePath(new ExactMatcher(true, absolutePath), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, absolutePath);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtAbsolutePath(IStringMatcher absolutePathMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => absolutePathMatcher.IsPerfectMatch(request.AbsolutePath));
var absolutePath = absolutePathMatcher.GetPatterns().FirstOrDefault().GetPattern();
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute path {0}{reason}, but no calls were made.",
absolutePath
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute path {0}{reason}, but didn't find it among the calls to {1}.",
_ => absolutePath,
requests => requests.Select(request => request.AbsolutePath)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, absolutePathMatcher);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> AtPath(string path, string because = "", params object[] becauseArgs)
{
_ = AtPath(new ExactMatcher(true, path), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, path);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtPath(IStringMatcher pathMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => pathMatcher.IsPerfectMatch(request.Path));
var path = pathMatcher.GetPatterns().FirstOrDefault().GetPattern();
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the path {0}{reason}, but no calls were made.",
path
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the path {0}{reason}, but didn't find it among the calls to {1}.",
_ => path,
requests => requests.Select(request => request.Path)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, pathMatcher);
}
}

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net
#if GRAPHQL
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -14,17 +13,17 @@ using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Exceptions;
using WireMock.Extensions;
using WireMock.Matchers.Models;
using WireMock.GraphQL.Models;
using WireMock.Models;
using WireMock.Util;
using WireMock.Models.GraphQL;
using WireMock.Utils;
namespace WireMock.Matchers;
/// <summary>
/// GrapQLMatcher Schema Matcher
/// </summary>
/// <inheritdoc cref="IStringMatcher"/>
public class GraphQLMatcher : IStringMatcher
public class GraphQLMatcher : IGraphQLMatcher
{
private sealed class GraphQLRequest
{
@@ -54,7 +53,7 @@ public class GraphQLMatcher : IStringMatcher
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
public GraphQLMatcher(
AnyOf<string, StringPattern, ISchema> schema,
AnyOf<string, StringPattern, ISchemaData> schema,
MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch,
MatchOperator matchOperator = MatchOperator.Or
) : this(schema, null, matchBehaviour, matchOperator)
@@ -69,7 +68,7 @@ public class GraphQLMatcher : IStringMatcher
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
public GraphQLMatcher(
AnyOf<string, StringPattern, ISchema> schema,
AnyOf<string, StringPattern, ISchemaData> schema,
IDictionary<string, Type>? customScalars,
MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch,
MatchOperator matchOperator = MatchOperator.Or
@@ -94,7 +93,7 @@ public class GraphQLMatcher : IStringMatcher
break;
case AnyOfType.Third:
_schema = schema.Third;
_schema = ((SchemaDataWrapper)schema.Third).Schema;
break;
default:
@@ -201,7 +200,7 @@ public class GraphQLMatcher : IStringMatcher
throw new WireMockException($"The GraphQL Scalar type '{scalarTypeDefinitionName}' is not defined in the CustomScalars dictionary.");
}
// Create a this custom Scalar GraphType (extending the WireMockCustomScalarGraphType<{clrType}> class)
// Create a custom Scalar GraphType (extending the WireMockCustomScalarGraphType<{clrType}> class)
var customScalarGraphType = ReflectionUtils.CreateGenericType(customScalarGraphTypeName, typeof(WireMockCustomScalarGraphType<>), clrType);
schema.RegisterType(customScalarGraphType);
}
@@ -209,5 +208,4 @@ public class GraphQLMatcher : IStringMatcher
return schema;
}
}
#endif
}

View File

@@ -0,0 +1,18 @@
// Copyright © WireMock.Net
using GraphQL.Types;
using WireMock.Models.GraphQL;
namespace WireMock.Models;
/// <summary>
/// Represents a wrapper for schema data, providing access to the associated schema.
/// </summary>
/// <param name="schema"></param>
public class SchemaDataWrapper(ISchema schema) : ISchemaData
{
/// <summary>
/// Gets the schema associated with the current instance.
/// </summary>
public ISchema Schema { get; } = schema;
}

View File

@@ -1,10 +1,10 @@
// Copyright © WireMock.Net
#if GRAPHQL
using System;
using GraphQL.Types;
namespace WireMock.Matchers.Models;
// ReSharper disable once CheckNamespace
namespace WireMock.GraphQL.Models;
/// <inheritdoc />
public abstract class WireMockCustomScalarGraphType<T> : ScalarGraphType
@@ -28,5 +28,4 @@ public abstract class WireMockCustomScalarGraphType<T> : ScalarGraphType
return (T)Convert.ChangeType(value, typeof(T));
}
}
#endif
}

View File

@@ -0,0 +1,8 @@
// Copyright © WireMock.Net
using System.Runtime.CompilerServices;
// [assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
// Needed for Moq in the UnitTest project
// [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@@ -0,0 +1,94 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using GraphQL.Types;
using Stef.Validation;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.Models.GraphQL;
namespace WireMock.RequestBuilders;
/// <summary>
/// IRequestBuilderExtensions extensions for GraphQL.
/// </summary>
// ReSharper disable once InconsistentNaming
public static class IRequestBuilderExtensions
{
/// <summary>
/// WithBodyAsGraphQL: The GraphQL body as a string.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a string.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a <see cref="ISchema"/>.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, new SchemaDataWrapper(schema)));
}
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a <see cref="ISchema"/>.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, new SchemaDataWrapper(schema), customScalars));
}
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a <see cref="ISchemaData"/>.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, ISchemaData schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a <see cref="ISchemaData"/>.
/// </summary>
/// <param name="requestBuilder">The <see cref="IRequestBuilder"/>.</param>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder WithGraphQLSchema(this IRequestBuilder requestBuilder, ISchemaData schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Guard.NotNull(requestBuilder).Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
}

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace WireMock.Util;
namespace WireMock.Utils;
internal static class ReflectionUtils
{

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>GraphQL support for WireMock.Net</Description>
<AssemblyTitle>WireMock.Net.Matchers.GraphQL</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;matchers;matcher;graphql</PackageTags>
<RootNamespace>WireMock</RootNamespace>
<ProjectGuid>{B6269AAC-170A-4346-8B9A-444DED3D9A45}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<!--<DelaySign>true</DelaySign>-->
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQL.NewtonsoftJson" Version="8.2.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="System.Reflection.Emit" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Shared\WireMock.Net.Shared.csproj" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -18,7 +18,7 @@ namespace WireMock.Matchers;
/// CSharpCode / CS-Script Matcher
/// </summary>
/// <inheritdoc cref="ICSharpCodeMatcher"/>
internal class CSharpCodeMatcher : ICSharpCodeMatcher
public class CSharpCodeMatcher : ICSharpCodeMatcher
{
private const string TemplateForIsMatchWithString = "public class CodeHelper {{ public bool IsMatch(string it) {{ {0} }} }}";
@@ -63,17 +63,24 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher
Value = patterns;
}
public MatchResult IsMatch(string? input)
/// <inheritdoc />
public MatchResult IsMatch(string? input) => IsMatchInternal(input);
/// <inheritdoc />
public MatchResult IsMatch(object? input) => IsMatchInternal(input);
/// <inheritdoc />
public string GetCSharpCodeArguments()
{
return IsMatchInternal(input);
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{MatchOperator.GetFullyQualifiedEnumValue()}, " +
$"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}" +
$")";
}
public MatchResult IsMatch(object? input)
{
return IsMatchInternal(input);
}
public MatchResult IsMatchInternal(object? input)
private MatchResult IsMatchInternal(object? input)
{
var score = MatchScores.Mismatch;
Exception? exception = null;
@@ -93,17 +100,6 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher
return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
}
/// <inheritdoc />
public string GetCSharpCodeArguments()
{
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{MatchOperator.GetFullyQualifiedEnumValue()}, " +
$"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}" +
$")";
}
private bool IsMatch(dynamic input, string pattern)
{
var isMatchWithString = input is string;

View File

@@ -1,7 +1,6 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -33,16 +32,25 @@ internal class MimeKitUtils : IMimeKitUtils
StartsWithMultiPart(contentTypeHeader)
)
{
var bytes = requestMessage.BodyData?.DetectedBodyType switch
byte[] bytes;
switch (requestMessage.BodyData?.DetectedBodyType)
{
// If the body is bytes, use the BodyAsBytes to match on.
BodyType.Bytes => requestMessage.BodyData.BodyAsBytes!,
case BodyType.Bytes:
bytes = requestMessage.BodyData.BodyAsBytes!;
break;
// If the body is a String or MultiPart, use the BodyAsString to match on.
BodyType.String or BodyType.MultiPart => Encoding.UTF8.GetBytes(requestMessage.BodyData.BodyAsString!),
case BodyType.String or BodyType.MultiPart:
bytes = Encoding.UTF8.GetBytes(requestMessage.BodyData.BodyAsString!);
break;
_ => throw new NotSupportedException()
};
// Else return false.
default:
mimeMessageData = null;
return false;
}
var fixedBytes = FixBytes(bytes, contentTypeHeader[0]);

View File

@@ -15,8 +15,13 @@ internal static class HttpClientBuilder
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = false,
#if NET5_0_OR_GREATER
SslProtocols = System.Security.Authentication.SslProtocols.Tls13 | System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls,
#else
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls,
#endif
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
#elif NET46
@@ -62,7 +67,10 @@ internal static class HttpClientBuilder
}
}
#if !NETSTANDARD1_3
#if NET5_0_OR_GREATER
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, errors) => true;
#elif !NETSTANDARD1_3
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, errors) => true;
#endif

View File

@@ -11,24 +11,17 @@ using Stef.Validation;
using WireMock.Models;
using WireMock.Settings;
using WireMock.Transformers;
using WireMock.Transformers.Handlebars;
using WireMock.Transformers.Scriban;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Http;
internal class WebhookSender
internal class WebhookSender(WireMockServerSettings settings)
{
private const string ClientIp = "::1";
private static readonly ThreadLocal<Random> Random = new(() => new Random(DateTime.UtcNow.Millisecond));
private readonly WireMockServerSettings _settings;
public WebhookSender(WireMockServerSettings settings)
{
_settings = Guard.NotNull(settings);
}
private readonly WireMockServerSettings _settings = Guard.NotNull(settings);
public async Task<HttpResponseMessage> SendAsync(
HttpClient client,
@@ -49,24 +42,7 @@ internal class WebhookSender
string requestUrl;
if (webhookRequest.UseTransformer == true)
{
ITransformer transformer;
switch (webhookRequest.TransformerType)
{
case TransformerType.Handlebars:
var factoryHandlebars = new HandlebarsContextFactory(_settings);
transformer = new Transformer(_settings, factoryHandlebars);
break;
case TransformerType.Scriban:
case TransformerType.ScribanDotLiquid:
var factoryDotLiquid = new ScribanContextFactory(_settings.FileSystemHandler, webhookRequest.TransformerType);
transformer = new Transformer(_settings, factoryDotLiquid);
break;
default:
throw new NotImplementedException($"TransformerType '{webhookRequest.TransformerType}' is not supported.");
}
var transformer = TransformerFactory.Create(webhookRequest.TransformerType, _settings);
bodyData = transformer.TransformBody(mapping, originalRequestMessage, originalResponseMessage, webhookRequest.BodyData, webhookRequest.TransformerReplaceNodeOptions);
headers = transformer.TransformHeaders(mapping, originalRequestMessage, originalResponseMessage, webhookRequest.Headers);
requestUrl = transformer.TransformString(mapping, originalRequestMessage, originalResponseMessage, webhookRequest.Url);

View File

@@ -43,7 +43,7 @@ public class Mapping : IMapping
public string? NextState { get; }
/// <inheritdoc />
public int? StateTimes { get; }
public int? TimesInSameState { get; }
/// <inheritdoc />
public IRequestMatcher RequestMatcher { get; }
@@ -137,7 +137,7 @@ public class Mapping : IMapping
Scenario = scenario;
ExecutionConditionState = executionConditionState;
NextState = nextState;
StateTimes = stateTimes;
TimesInSameState = stateTimes;
Webhooks = webhooks;
UseWebhooksFireAndForget = useWebhooksFireAndForget;
TimeSettings = timeSettings;

View File

@@ -0,0 +1,60 @@
// Copyright © WireMock.Net
using System;
using Newtonsoft.Json.Linq;
using Stef.Validation;
namespace WireMock.Matchers.Request;
/// <summary>
/// The request body matcher.
/// </summary>
public class RequestMessageBodyMatcher<T> : IRequestMatcher
{
/// <summary>
/// The body data function for type T
/// </summary>
public Func<T?, bool>? Func { get; }
/// <summary>
/// The <see cref="MatchOperator"/>
/// </summary>
public MatchOperator MatchOperator { get; } = MatchOperator.Or;
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<T?, bool> func)
{
Func = Guard.NotNull(func);
}
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
var (score, exception) = CalculateMatchScore(requestMessage).Expand();
return requestMatchResult.AddScore(GetType(), score, exception);
}
private MatchResult CalculateMatchScore(IRequestMessage requestMessage)
{
if (Func != null)
{
if (requestMessage.BodyData?.BodyAsJson is JObject jsonObject)
{
try
{
var bodyAsT = jsonObject.ToObject<T>();
return MatchScores.ToScore(Func(bodyAsT));
}
catch (Exception ex)
{
return new MatchResult(ex);
}
}
}
return default;
}
}

View File

@@ -12,7 +12,7 @@ namespace WireMock.Matchers.Request;
/// </summary>
public class RequestMessageMultiPartMatcher : IRequestMatcher
{
private static readonly IMimeKitUtils MimeKitUtils = TypeLoader.LoadStaticInstance<IMimeKitUtils>();
private readonly IMimeKitUtils _mimeKitUtils = LoadMimeKitUtils();
/// <summary>
/// The matchers.
@@ -62,7 +62,7 @@ public class RequestMessageMultiPartMatcher : IRequestMatcher
return requestMatchResult.AddScore(GetType(), score, null);
}
if (!MimeKitUtils.TryGetMimeMessage(requestMessage, out var message))
if (!_mimeKitUtils.TryGetMimeMessage(requestMessage, out var message))
{
return requestMatchResult.AddScore(GetType(), score, null);
}
@@ -96,4 +96,14 @@ public class RequestMessageMultiPartMatcher : IRequestMatcher
return requestMatchResult.AddScore(GetType(), score, exception);
}
private static IMimeKitUtils LoadMimeKitUtils()
{
if (TypeLoader.TryLoadStaticInstance<IMimeKitUtils>(out var mimeKitUtils))
{
return mimeKitUtils;
}
throw new InvalidOperationException("MimeKit is required for RequestMessageMultiPartMatcher. Please install the WireMock.Net.MimePart package.");
}
}

View File

@@ -177,11 +177,13 @@ namespace WireMock.Owin.Mappers
var jsonBody = JsonConvert.SerializeObject(bodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore });
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
#if PROTOBUF
case BodyType.ProtoBuf:
var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
return await ProtoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
#endif
if (TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
{
var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
return await protoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
}
break;
case BodyType.Bytes:
return bodyData.BodyAsBytes;

View File

@@ -9,16 +9,10 @@ using WireMock.Services;
namespace WireMock.Owin;
internal class MappingMatcher : IMappingMatcher
internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1) : IMappingMatcher
{
private readonly IWireMockMiddlewareOptions _options;
private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1;
public MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDoubleBetween0And1 randomizerDoubleBetween0And1)
{
_options = Guard.NotNull(options);
_randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1);
}
private readonly IWireMockMiddlewareOptions _options = Guard.NotNull(options);
private readonly IRandomizerDoubleBetween0And1 _randomizerDoubleBetween0And1 = Guard.NotNull(randomizerDoubleBetween0And1);
public (MappingMatcherResult? Match, MappingMatcherResult? Partial) FindBestMatch(RequestMessage request)
{
@@ -28,7 +22,7 @@ internal class MappingMatcher : IMappingMatcher
var mappings = _options.Mappings.Values
.Where(m => m.TimeSettings.IsValid())
.Where(m => m.Probability is null || m.Probability <= _randomizerDoubleBetween0And1.Generate())
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
.ToArray();
foreach (var mapping in mappings)
@@ -41,10 +35,10 @@ internal class MappingMatcher : IMappingMatcher
var exceptions = mappingMatcherResult.RequestMatchResult.MatchDetails
.Where(md => md.Exception != null)
.Select(md => md.Exception)
.Select(md => md.Exception!)
.ToArray();
if (!exceptions.Any())
if (exceptions.Length == 0)
{
possibleMappings.Add(mappingMatcherResult);
}
@@ -52,7 +46,7 @@ internal class MappingMatcher : IMappingMatcher
{
foreach (var ex in exceptions)
{
LogException(mapping, ex!);
LogException(mapping, ex);
}
}
}
@@ -62,14 +56,16 @@ internal class MappingMatcher : IMappingMatcher
}
}
var partialMappings = possibleMappings
var partialMatches = possibleMappings
.Where(pm => (pm.Mapping.IsAdminInterface && pm.RequestMatchResult.IsPerfectMatch) || !pm.Mapping.IsAdminInterface)
.OrderBy(m => m.RequestMatchResult)
.ThenBy(m => m.RequestMatchResult.TotalNumber)
.ThenBy(m => m.Mapping.Priority)
.ThenByDescending(m => m.Mapping.Probability)
.ThenByDescending(m => m.Mapping.UpdatedAt)
.ToList();
var partialMatch = partialMappings.FirstOrDefault(pm => pm.RequestMatchResult.AverageTotalScore > 0.0);
.Where(pm => pm.RequestMatchResult.AverageTotalScore > 0.0)
.ToArray();
var partialMatch = partialMatches.FirstOrDefault();
if (_options.AllowPartialMapping == true)
{
@@ -78,7 +74,11 @@ internal class MappingMatcher : IMappingMatcher
var match = possibleMappings
.Where(m => m.RequestMatchResult.IsPerfectMatch)
.OrderBy(m => m.Mapping.Priority).ThenBy(m => m.RequestMatchResult).ThenByDescending(m => m.Mapping.UpdatedAt)
.OrderBy(m => m.Mapping.Priority)
.ThenBy(m => m.RequestMatchResult)
.ThenBy(m => m.RequestMatchResult.TotalNumber)
.ThenByDescending(m => m.Mapping.Probability)
.ThenByDescending(m => m.Mapping.UpdatedAt)
.FirstOrDefault();
return (match, partialMatch);

View File

@@ -299,7 +299,7 @@ namespace WireMock.Owin
scenario.Counter++;
// Only if the number of times this state is executed equals the required StateTimes, proceed to next state and reset the counter to 0
if (scenario.Counter == (mapping.StateTimes ?? 1))
if (scenario.Counter == (mapping.TimesInSameState ?? 1))
{
scenario.NextState = mapping.NextState;
scenario.Counter = 0;

View File

@@ -13,16 +13,10 @@ using WireMock.Util;
namespace WireMock.Proxy;
internal class ProxyHelper
internal class ProxyHelper(WireMockServerSettings settings)
{
private readonly WireMockServerSettings _settings;
private readonly ProxyMappingConverter _proxyMappingConverter;
public ProxyHelper(WireMockServerSettings settings)
{
_settings = Guard.NotNull(settings);
_proxyMappingConverter = new ProxyMappingConverter(settings, new GuidUtils(), new DateTimeUtils());
}
private readonly WireMockServerSettings _settings = Guard.NotNull(settings);
private readonly ProxyMappingConverter _proxyMappingConverter = new(settings, new GuidUtils(), new DateTimeUtils());
public async Task<(IResponseMessage Message, IMapping? Mapping)> SendAsync(
IMapping? mapping,
@@ -39,18 +33,7 @@ internal class ProxyHelper
var requiredUri = new Uri(url);
// Create HttpRequestMessage
var replaceSettings = proxyAndRecordSettings.ReplaceSettings;
string proxyUrl;
if (replaceSettings is not null)
{
var stringComparison = replaceSettings.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
proxyUrl = url.Replace(replaceSettings.OldValue, replaceSettings.NewValue, stringComparison);
}
else
{
proxyUrl = url;
}
var proxyUrl = proxyAndRecordSettings.ReplaceSettings != null ? ProxyUrlTransformer.Transform(_settings, proxyAndRecordSettings.ReplaceSettings, url) : url;
var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, proxyUrl);
// Call the URL

View File

@@ -0,0 +1,21 @@
// Copyright © WireMock.Net
using System;
using WireMock.Settings;
using WireMock.Transformers;
namespace WireMock.Proxy;
internal static class ProxyUrlTransformer
{
internal static string Transform(WireMockServerSettings settings, ProxyUrlReplaceSettings replaceSettings, string url)
{
if (!replaceSettings.UseTransformer)
{
return url.Replace(replaceSettings.OldValue, replaceSettings.NewValue, replaceSettings.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
var transformer = TransformerFactory.Create(replaceSettings.TransformerType, settings);
return transformer.Transform(replaceSettings.TransformTemplate, url);
}
}

View File

@@ -1,83 +0,0 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Matchers;
namespace WireMock.RequestBuilders;
/// <summary>
/// The GraphQLRequestBuilder interface.
/// </summary>
public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
{
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
#if GRAPHQL
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
#endif
}

View File

@@ -1,67 +0,0 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using WireMock.Matchers;
namespace WireMock.RequestBuilders;
/// <summary>
/// The ProtoBufRequestBuilder interface.
/// </summary>
public interface IProtoBufRequestBuilder : IGraphQLRequestBuilder
{
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The matcher to use to match the ProtoBuf as (json) object.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="protoDefinitions">The proto definitions as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(IReadOnlyList<string> protoDefinitions, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="protoDefinitions">The proto definitions as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The matcher to use to match the ProtoBuf as (json) object.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(IReadOnlyList<string> protoDefinitions, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsProtoBuf
/// </summary>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The matcher to use to match the ProtoBuf as (json) object.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
}

View File

@@ -1,10 +0,0 @@
// Copyright © WireMock.Net
namespace WireMock.RequestBuilders;
/// <summary>
/// IRequestBuilder
/// </summary>
public interface IRequestBuilder : IClientIPRequestBuilder
{
}

View File

@@ -34,13 +34,6 @@ public partial class Request
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
return WithBody([matcher]);
}
/// <inheritdoc />
public IRequestBuilder WithBody(IMatcher matcher)
{
@@ -100,8 +93,18 @@ public partial class Request
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQLSchema(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithGraphQLSchema(body, matchBehaviour);
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
return WithBody([matcher]);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsType<T>(Func<T?, bool> func)
{
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher<T>(func));
return this;
}
}

View File

@@ -1,61 +0,0 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsProtoBuf([protoDefinition], messageType, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsProtoBuf([protoDefinition], messageType, matcher, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(IReadOnlyList<string> protoDefinitions, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new IdOrTexts(null, protoDefinitions), messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(IReadOnlyList<string> protoDefinitions, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new IdOrTexts(null, protoDefinitions), messageType, matcher));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType, matcher));
}
private Func<IdOrTexts> ProtoDefinitionFunc()
{
return () =>
{
if (Mapping.ProtoDefinition == null)
{
throw new InvalidOperationException($"No ProtoDefinition defined on mapping '{Mapping.Guid}'. Please use the WireMockServerSettings to define ProtoDefinitions.");
}
return Mapping.ProtoDefinition.Value;
};
}
}

View File

@@ -1,61 +0,0 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System;
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, customScalars, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
#if GRAPHQL
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, customScalars, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
#endif
}

View File

@@ -4,6 +4,7 @@ using System;
using Stef.Validation;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Validators;
namespace WireMock.RequestBuilders;
@@ -34,6 +35,10 @@ public partial class Request
public IRequestBuilder WithPath(MatchOperator matchOperator, params string[] paths)
{
Guard.NotNullOrEmpty(paths);
foreach (var path in paths)
{
PathValidator.ValidateAndThrow(path, nameof(paths));
}
_requestMatchers.Add(new RequestMessagePathMatcher(MatchBehaviour.AcceptOnMatch, matchOperator, paths));
return this;

View File

@@ -20,9 +20,7 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
{
private readonly IList<IRequestMatcher> _requestMatchers;
/// <summary>
/// The link back to the Mapping.
/// </summary>
/// <inheritdoc />
public IMapping Mapping { get; set; } = null!;
/// <summary>
@@ -31,7 +29,7 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder Create()
{
return new Request(new List<IRequestMatcher>());
return new Request([]);
}
/// <summary>
@@ -73,6 +71,18 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
return _requestMatchers.OfType<T>().FirstOrDefault(func);
}
/// <inheritdoc />
public IRequestBuilder Add<T>(T requestMatcher) where T : IRequestMatcher
{
foreach (var existing in _requestMatchers.OfType<T>().ToArray())
{
_requestMatchers.Remove(existing);
}
_requestMatchers.Add(requestMatcher);
return this;
}
internal bool TryGetProtoBufMatcher([NotNullWhen(true)] out IProtoBufMatcher? protoBufMatcher)
{
protoBufMatcher = GetRequestMessageMatcher<RequestMessageProtoBufMatcher>()?.Matcher;
@@ -85,15 +95,4 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
protoBufMatcher = bodyMatcher?.Matchers?.OfType<IProtoBufMatcher>().FirstOrDefault();
return protoBufMatcher != null;
}
private IRequestBuilder Add<T>(T requestMatcher) where T : IRequestMatcher
{
foreach (var existing in _requestMatchers.OfType<T>().ToArray())
{
_requestMatchers.Remove(existing);
}
_requestMatchers.Add(requestMatcher);
return this;
}
}

View File

@@ -183,16 +183,9 @@ public class RequestMessage : IRequestMessage
#endif
#if MIMEKIT
try
if (TypeLoader.TryLoadStaticInstance<IMimeKitUtils>(out var mimeKitUtils) && mimeKitUtils.TryGetMimeMessage(this, out var mimeMessage))
{
if (TypeLoader.LoadStaticInstance<IMimeKitUtils>().TryGetMimeMessage(this, out var mimeMessage))
{
BodyAsMimeMessage = mimeMessage;
}
}
catch
{
// Ignore exception from MimeMessage.Load
BodyAsMimeMessage = mimeMessage;
}
#endif
}

View File

@@ -1,10 +0,0 @@
// Copyright © WireMock.Net
namespace WireMock.ResponseBuilders;
/// <summary>
/// The ResponseBuilder interface.
/// </summary>
public interface IResponseBuilder : IProxyResponseBuilder
{
}

View File

@@ -1,12 +1,10 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JsonConverter.Abstractions;
using Stef.Validation;
using WireMock.Exceptions;
using WireMock.Models;
using WireMock.Types;
using WireMock.Util;
@@ -235,72 +233,4 @@ public partial class Response
return this;
}
/// <inheritdoc />
public IResponseBuilder WithBodyAsProtoBuf(
string protoDefinition,
string messageType,
object value,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? options = null
)
{
return WithBodyAsProtoBuf([protoDefinition], messageType, value, jsonConverter, options);
}
/// <inheritdoc />
public IResponseBuilder WithBodyAsProtoBuf(
IReadOnlyList<string> protoDefinitions,
string messageType,
object value,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? options = null
)
{
Guard.NotNullOrEmpty(protoDefinitions);
Guard.NotNullOrWhiteSpace(messageType);
Guard.NotNull(value);
#if !PROTOBUF
throw new NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#else
ResponseMessage.BodyDestination = null;
ResponseMessage.BodyData = new BodyData
{
DetectedBodyType = BodyType.ProtoBuf,
BodyAsJson = value,
ProtoDefinition = () => new IdOrTexts(null, protoDefinitions),
ProtoBufMessageType = messageType
};
return this;
#endif
}
/// <inheritdoc />
public IResponseBuilder WithBodyAsProtoBuf(
string messageType,
object value,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? options = null
)
{
Guard.NotNullOrWhiteSpace(messageType);
Guard.NotNull(value);
#if !PROTOBUF
throw new NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#else
ResponseMessage.BodyDestination = null;
ResponseMessage.BodyData = new BodyData
{
DetectedBodyType = BodyType.ProtoBuf,
BodyAsJson = value,
ProtoDefinition = () => Mapping.ProtoDefinition ?? throw new WireMockException("ProtoDefinition cannot be resolved. You probably forgot to call .WithProtoDefinition(...) on the mapping."),
ProtoBufMessageType = messageType
};
return this;
#endif
}
}

View File

@@ -13,13 +13,13 @@ public partial class Response
/// A delegate to execute to generate the response.
/// </summary>
[MemberNotNullWhen(true, nameof(WithCallbackUsed))]
public Func<IRequestMessage, ResponseMessage>? Callback { get; private set; }
public Func<IRequestMessage, IResponseMessage>? Callback { get; private set; }
/// <summary>
/// A delegate to execute to generate the response async.
/// </summary>
[MemberNotNullWhen(true, nameof(WithCallbackUsed))]
public Func<IRequestMessage, Task<ResponseMessage>>? CallbackAsync { get; private set; }
public Func<IRequestMessage, Task<IResponseMessage>>? CallbackAsync { get; private set; }
/// <summary>
/// Defines if the method WithCallback(...) is used.
@@ -27,7 +27,7 @@ public partial class Response
public bool WithCallbackUsed { get; private set; }
/// <inheritdoc />
public IResponseBuilder WithCallback(Func<IRequestMessage, ResponseMessage> callbackHandler)
public IResponseBuilder WithCallback(Func<IRequestMessage, IResponseMessage> callbackHandler)
{
Guard.NotNull(callbackHandler);
@@ -35,14 +35,14 @@ public partial class Response
}
/// <inheritdoc />
public IResponseBuilder WithCallback(Func<IRequestMessage, Task<ResponseMessage>> callbackHandler)
public IResponseBuilder WithCallback(Func<IRequestMessage, Task<IResponseMessage>> callbackHandler)
{
Guard.NotNull(callbackHandler);
return WithCallbackInternal(true, callbackHandler);
}
private IResponseBuilder WithCallbackInternal(bool withCallbackUsed, Func<IRequestMessage, ResponseMessage> callbackHandler)
private IResponseBuilder WithCallbackInternal(bool withCallbackUsed, Func<IRequestMessage, IResponseMessage> callbackHandler)
{
Guard.NotNull(callbackHandler);
@@ -52,7 +52,7 @@ public partial class Response
return this;
}
private IResponseBuilder WithCallbackInternal(bool withCallbackUsed, Func<IRequestMessage, Task<ResponseMessage>> callbackHandler)
private IResponseBuilder WithCallbackInternal(bool withCallbackUsed, Func<IRequestMessage, Task<IResponseMessage>> callbackHandler)
{
Guard.NotNull(callbackHandler);

View File

@@ -28,9 +28,7 @@ public partial class Response : IResponseBuilder
private TimeSpan? _delay;
/// <summary>
/// The link back to the mapping.
/// </summary>
/// <inheritdoc />
public IMapping Mapping { get; set; } = null!;
/// <summary>
@@ -81,10 +79,8 @@ public partial class Response : IResponseBuilder
/// </summary>
public ReplaceNodeOptions TransformerReplaceNodeOptions { get; private set; }
/// <summary>
/// Gets the response message.
/// </summary>
public ResponseMessage ResponseMessage { get; }
/// <inheritdoc />
public IResponseMessage ResponseMessage { get; }
/// <summary>
/// Creates this instance.
@@ -226,7 +222,7 @@ public partial class Response : IResponseBuilder
).ConfigureAwait(false);
}
ResponseMessage responseMessage;
IResponseMessage responseMessage;
if (!WithCallbackUsed)
{
responseMessage = ResponseMessage;
@@ -276,25 +272,8 @@ public partial class Response : IResponseBuilder
}
}
ITransformer responseMessageTransformer;
switch (TransformerType)
{
case TransformerType.Handlebars:
var factoryHandlebars = new HandlebarsContextFactory(settings);
responseMessageTransformer = new Transformer(settings, factoryHandlebars);
break;
case TransformerType.Scriban:
case TransformerType.ScribanDotLiquid:
var factoryDotLiquid = new ScribanContextFactory(settings.FileSystemHandler, TransformerType);
responseMessageTransformer = new Transformer(settings, factoryDotLiquid);
break;
default:
throw new NotSupportedException($"TransformerType '{TransformerType}' is not supported.");
}
return (responseMessageTransformer.Transform(mapping, requestMessage, responseMessage, UseTransformerForBodyAsFile, TransformerReplaceNodeOptions), null);
var transformer = TransformerFactory.Create(TransformerType, settings);
return (transformer.Transform(mapping, requestMessage, responseMessage, UseTransformerForBodyAsFile, TransformerReplaceNodeOptions), null);
}
if (!UseTransformer && ResponseMessage.BodyData?.BodyAsFileIsCached == true && responseMessage.BodyData?.BodyAsFile is not null)

View File

@@ -13,7 +13,6 @@ using WireMock.Constants;
using WireMock.Extensions;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Types;
@@ -111,15 +110,13 @@ internal class MappingConverter(MatcherMapper mapper)
sb.AppendLine($" .WithHttpVersion({requestMessageHttpVersionMatcher.HttpVersion})");
}
#if GRAPHQL
if (requestMessageGraphQLMatcher?.Matchers != null)
{
if (requestMessageGraphQLMatcher.Matchers.OfType<GraphQLMatcher>().FirstOrDefault() is { } graphQLMatcher && graphQLMatcher.GetPatterns().Any())
if (requestMessageGraphQLMatcher.Matchers.OfType<IGraphQLMatcher>().FirstOrDefault() is { } graphQLMatcher && graphQLMatcher.GetPatterns().Any())
{
sb.AppendLine($" .WithGraphQLSchema({GetString(graphQLMatcher)})");
}
}
#endif
if (requestMessageMultiPartMatcher?.Matchers != null)
{
@@ -129,12 +126,10 @@ internal class MappingConverter(MatcherMapper mapper)
}
}
#if PROTOBUF
if (requestMessageProtoBufMatcher?.Matcher != null)
{
sb.AppendLine(" // .WithBodyAsProtoBuf() is not yet supported");
}
#endif
if (requestMessageBodyMatcher?.Matchers != null)
{
@@ -164,6 +159,16 @@ internal class MappingConverter(MatcherMapper mapper)
// Guid
sb.AppendLine($" .WithGuid(\"{mapping.Guid}\")");
// Scenario + State
if (!string.IsNullOrEmpty(mapping.Scenario))
{
sb.AppendLine($" .InScenario({ToCSharpStringLiteral(mapping.Scenario)})");
}
if (!string.IsNullOrEmpty(mapping.NextState))
{
sb.AppendLine($" .WhenStateIs({ToCSharpStringLiteral(mapping.NextState)}, {ToCSharpIntLiteral(mapping.TimesInSameState)})");
}
if (mapping.Probability != null)
{
sb.AppendLine($" .WithProbability({mapping.Probability.Value.ToString(CultureInfoUtils.CultureInfoEnUS)})");
@@ -181,7 +186,7 @@ internal class MappingConverter(MatcherMapper mapper)
sb.AppendLine($" .WithStatusCode({(int)httpStatusCode})");
}
if (response.ResponseMessage.Headers is { })
if (response.ResponseMessage.Headers != null)
{
foreach (var header in response.ResponseMessage.Headers)
{
@@ -189,7 +194,7 @@ internal class MappingConverter(MatcherMapper mapper)
}
}
if (response.ResponseMessage.TrailingHeaders is { })
if (response.ResponseMessage.TrailingHeaders != null)
{
foreach (var header in response.ResponseMessage.TrailingHeaders)
{
@@ -221,14 +226,14 @@ internal class MappingConverter(MatcherMapper mapper)
}
}
if (response.Delay is { })
{
sb.AppendLine($" .WithDelay({response.Delay.Value.TotalMilliseconds})");
}
else if (response is { MinimumDelayMilliseconds: > 0, MaximumDelayMilliseconds: > 0 })
if (response is { MinimumDelayMilliseconds: > 0, MaximumDelayMilliseconds: > 0 })
{
sb.AppendLine($" .WithRandomDelay({response.MinimumDelayMilliseconds}, {response.MaximumDelayMilliseconds})");
}
else if (response.Delay is { })
{
sb.AppendLine($" .WithDelay({response.Delay.Value.TotalMilliseconds})");
}
if (response.UseTransformer)
{
@@ -270,6 +275,7 @@ internal class MappingConverter(MatcherMapper mapper)
Scenario = mapping.Scenario,
WhenStateIs = mapping.ExecutionConditionState,
SetStateTo = mapping.NextState,
TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null,
Data = mapping.Data,
Probability = mapping.Probability,
Request = new RequestModel
@@ -392,13 +398,11 @@ internal class MappingConverter(MatcherMapper mapper)
{
void AfterMap(MatcherModel matcherModel)
{
#if PROTOBUF
// In case the ProtoDefinition is defined at the Mapping level, clear the Pattern at the Matcher level
if (bodyMatchers?.OfType<ProtoBufMatcher>().Any() == true && mappingModel.ProtoDefinition != null)
if (bodyMatchers?.OfType<IProtoBufMatcher>().Any() == true && mappingModel.ProtoDefinition != null)
{
matcherModel.Pattern = null;
}
#endif
}
mappingModel.Request.Body = new BodyModel();

View File

@@ -10,6 +10,7 @@ using WireMock.Admin.Mappings;
using WireMock.Extensions;
using WireMock.Matchers;
using WireMock.Models;
using WireMock.Models.GraphQL;
using WireMock.Settings;
using WireMock.Util;
@@ -54,7 +55,12 @@ internal class MatcherMapper
case "CSharpCodeMatcher":
if (_settings.AllowCSharpCodeMatcher == true)
{
return TypeLoader.LoadNewInstance<ICSharpCodeMatcher>(matchBehaviour, matchOperator, stringPatterns);
if (TypeLoader.TryLoadNewInstance<ICSharpCodeMatcher>(out var csharpCodeMatcher, matchBehaviour, matchOperator, stringPatterns))
{
return csharpCodeMatcher;
}
throw new InvalidOperationException("The 'CSharpCodeMatcher' cannot be loaded. Please install the WireMock.Net.Matchers.CSharpCode package.");
}
throw new NotSupportedException("It's not allowed to use the 'CSharpCodeMatcher' because WireMockServerSettings.AllowCSharpCodeMatcher is not set to 'true'.");
@@ -70,18 +76,23 @@ internal class MatcherMapper
case nameof(ExactObjectMatcher):
return CreateExactObjectMatcher(matchBehaviour, stringPatterns[0]);
#if GRAPHQL
case nameof(GraphQLMatcher):
return new GraphQLMatcher(stringPatterns[0].GetPattern(), matcherModel.CustomScalars, matchBehaviour, matchOperator);
#endif
case "GraphQLMatcher":
var patternAsString = stringPatterns[0].GetPattern();
var schema = new AnyOf<string, StringPattern, ISchemaData>(patternAsString);
if (TypeLoader.TryLoadNewInstance<IGraphQLMatcher>(out var graphQLMatcher, schema, matcherModel.CustomScalars, matchBehaviour, matchOperator))
{
return graphQLMatcher;
}
throw new InvalidOperationException("The 'GraphQLMatcher' cannot be loaded. Please install the WireMock.Net.GraphQL package.");
case "MimePartMatcher":
return CreateMimePartMatcher(matchBehaviour, matcherModel);
#if PROTOBUF
case nameof(ProtoBufMatcher):
case "ProtoBufMatcher":
return CreateProtoBufMatcher(matchBehaviour, stringPatterns.GetPatterns(), matcherModel);
#endif
case nameof(RegexMatcher):
return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, useRegexExtended, matchOperator);
@@ -165,11 +176,10 @@ internal class MatcherMapper
case XPathMatcher xpathMatcher:
model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
break;
#if GRAPHQL
case GraphQLMatcher graphQLMatcher:
case IGraphQLMatcher graphQLMatcher:
model.CustomScalars = graphQLMatcher.CustomScalars;
break;
#endif
}
switch (matcher)
@@ -209,7 +219,7 @@ internal class MatcherMapper
break;
#if PROTOBUF
case ProtoBufMatcher protoBufMatcher:
case IProtoBufMatcher protoBufMatcher:
protoBufMatcher.ProtoDefinition().Value(id => model.Pattern = id, texts =>
{
if (texts.Count == 1)
@@ -277,25 +287,39 @@ internal class MatcherMapper
private IMimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, MatcherModel matcher)
{
var contentTypeMatcher = Map(matcher?.ContentTypeMatcher) as IStringMatcher;
var contentDispositionMatcher = Map(matcher?.ContentDispositionMatcher) as IStringMatcher;
var contentTransferEncodingMatcher = Map(matcher?.ContentTransferEncodingMatcher) as IStringMatcher;
var contentMatcher = Map(matcher?.ContentMatcher);
var contentTypeMatcher = Map(matcher.ContentTypeMatcher) as IStringMatcher;
var contentDispositionMatcher = Map(matcher.ContentDispositionMatcher) as IStringMatcher;
var contentTransferEncodingMatcher = Map(matcher.ContentTransferEncodingMatcher) as IStringMatcher;
var contentMatcher = Map(matcher.ContentMatcher);
return TypeLoader.LoadNewInstance<IMimePartMatcher>(matchBehaviour, contentTypeMatcher, contentDispositionMatcher, contentTransferEncodingMatcher, contentMatcher);
if (TypeLoader.TryLoadNewInstance<IMimePartMatcher>(
out var mimePartMatcher,
matchBehaviour,
contentTypeMatcher,
contentDispositionMatcher,
contentTransferEncodingMatcher,
contentMatcher))
{
return mimePartMatcher;
}
throw new InvalidOperationException("The 'MimePartMatcher' cannot be loaded. Please install the WireMock.Net.MimePart package.");
}
#if PROTOBUF
private ProtoBufMatcher CreateProtoBufMatcher(MatchBehaviour? matchBehaviour, IReadOnlyList<string> protoDefinitions, MatcherModel matcher)
private IProtoBufMatcher CreateProtoBufMatcher(MatchBehaviour? matchBehaviour, IReadOnlyList<string> protoDefinitions, MatcherModel matcher)
{
var objectMatcher = Map(matcher.ContentMatcher) as IObjectMatcher;
return new ProtoBufMatcher(
() => ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitions.ToArray()),
if (TypeLoader.TryLoadNewInstance<IProtoBufMatcher>(
out var protobufMatcher,
() => ProtoDefinitionUtils.GetIdOrTexts(_settings, protoDefinitions.ToArray()),
matcher.ProtoBufMessageType!,
matchBehaviour ?? MatchBehaviour.AcceptOnMatch,
objectMatcher
);
objectMatcher))
{
return protobufMatcher;
}
throw new InvalidOperationException("The 'ProtoBufMatcher' cannot be loaded. Please install the WireMock.Net.ProtoBuf package.");
}
#endif
}

View File

@@ -356,12 +356,9 @@ internal class RespondWithAProvider : IRespondWithAProvider
{
Guard.NotNull(protoDefinitionOrId);
#if PROTOBUF
ProtoDefinition = ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitionOrId);
ProtoDefinition = ProtoDefinitionUtils.GetIdOrTexts(_settings, protoDefinitionOrId);
return this;
#else
throw new NotSupportedException("The WithProtoDefinition method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#endif
}
/// <inheritdoc />

View File

@@ -97,7 +97,7 @@ public partial class WireMockServer
if (!string.IsNullOrEmpty(mappingModel.SetStateTo))
{
respondProvider = respondProvider.WillSetStateTo(mappingModel.SetStateTo!);
respondProvider = respondProvider.WillSetStateTo(mappingModel.SetStateTo!, mappingModel.TimesInSameState);
}
}
@@ -241,13 +241,13 @@ public partial class WireMockServer
if (requestModel.Body?.Matcher != null)
{
var bodyMatcher = _matcherMapper.Map(requestModel.Body.Matcher)!;
#if PROTOBUF
// If the BodyMatcher is a ProtoBufMatcher, and if ProtoDefinition is defined on Mapping-level, set the ProtoDefinition from that Mapping.
if (bodyMatcher is ProtoBufMatcher protoBufMatcher && mappingModel?.ProtoDefinition != null)
if (bodyMatcher is IProtoBufMatcher protoBufMatcher && mappingModel?.ProtoDefinition != null)
{
protoBufMatcher.ProtoDefinition = () => ProtoDefinitionHelper.GetIdOrTexts(_settings, mappingModel.ProtoDefinition);
protoBufMatcher.ProtoDefinition = () => ProtoDefinitionUtils.GetIdOrTexts(_settings, mappingModel.ProtoDefinition);
}
#endif
requestBuilder = requestBuilder.WithBody(bodyMatcher);
}
else if (requestModel.Body?.Matchers != null)
@@ -366,20 +366,19 @@ public partial class WireMockServer
}
else if (responseModel.BodyAsJson != null)
{
if (responseModel.ProtoBufMessageType != null)
if (responseModel.ProtoBufMessageType != null && TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
{
if (responseModel.ProtoDefinition != null)
{
responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoDefinition, responseModel.ProtoBufMessageType, responseModel.BodyAsJson);
responseBuilder = protoBufUtils.UpdateResponseBuilder(responseBuilder, responseModel.ProtoBufMessageType, responseModel.BodyAsJson, responseModel.ProtoDefinition);
}
else if (responseModel.ProtoDefinitions != null)
{
responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoDefinitions, responseModel.ProtoBufMessageType, responseModel.BodyAsJson);
responseBuilder = protoBufUtils.UpdateResponseBuilder(responseBuilder, responseModel.ProtoBufMessageType, responseModel.BodyAsJson, responseModel.ProtoDefinitions);
}
else
{
// ProtoDefinition(s) is/are defined at Mapping/Server level
responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoBufMessageType, responseModel.BodyAsJson);
responseBuilder = protoBufUtils.UpdateResponseBuilder(responseBuilder, responseModel.ProtoBufMessageType, responseModel.BodyAsJson);
}
}
else

View File

@@ -1,24 +0,0 @@
// Copyright © WireMock.Net
namespace WireMock.Settings;
/// <summary>
/// Defines an old path param and a new path param to be replaced when proxying.
/// </summary>
public class ProxyUrlReplaceSettings
{
/// <summary>
/// The old path value to be replaced by the new path value
/// </summary>
public string OldValue { get; set; } = null!;
/// <summary>
/// The new path value to replace the old value with
/// </summary>
public string NewValue { get; set; } = null!;
/// <summary>
/// Defines if the case should be ignored when replacing.
/// </summary>
public bool IgnoreCase { get; set; }
}

View File

@@ -55,7 +55,7 @@ internal class SimpleSettingsParser
// Now also parse environment
if (environment != null)
{
foreach (string key in environment.Keys)
foreach (var key in environment.Keys.OfType<string>())
{
if (key.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && environment.TryGetStringValue(key, out var value))
{

View File

@@ -36,7 +36,7 @@ public static class WireMockServerSettingsParser
if (parser.GetBoolSwitchValue("help"))
{
(logger ?? new WireMockConsoleLogger()).Info("See https://github.com/wiremock/WireMock.Net/wiki/WireMock-commandline-parameters for details on all commandline options.");
(logger ?? new WireMockConsoleLogger()).Info("See https://wiremock.org/dotnet/wiremock-commandline-parameters/ for details on all commandline options.");
settings = null;
return false;
}
@@ -153,7 +153,7 @@ public static class WireMockServerSettingsParser
}
else if (settings.HostingScheme is null)
{
settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]);
settings.Urls = parser.GetValues(nameof(WireMockServerSettings.Urls), defaultValue: ["http://*:9091/"]);
}
}
@@ -202,14 +202,27 @@ public static class WireMockServerSettingsParser
private static void ParseProxyUrlReplaceSettings(ProxyAndRecordSettings settings, SimpleSettingsParser parser)
{
var proxyUrlReplaceOldValue = parser.GetStringValue("ProxyUrlReplaceOldValue");
var proxyUrlReplaceNewValue = parser.GetStringValue("ProxyUrlReplaceNewValue");
const string prefix = "ProxyUrlReplace";
var proxyUrlReplaceOldValue = parser.GetStringValue($"{prefix}OldValue");
var proxyUrlReplaceNewValue = parser.GetStringValue($"{prefix}NewValue");
if (!string.IsNullOrEmpty(proxyUrlReplaceOldValue) && proxyUrlReplaceNewValue != null)
{
settings.ReplaceSettings = new ProxyUrlReplaceSettings
{
OldValue = proxyUrlReplaceOldValue!,
NewValue = proxyUrlReplaceNewValue
OldValue = proxyUrlReplaceOldValue,
NewValue = proxyUrlReplaceNewValue,
IgnoreCase = parser.GetBoolValue($"{prefix}IgnoreCase")
};
return;
}
var transformTemplate = parser.GetStringValue($"{prefix}TransformTemplate");
if (!string.IsNullOrEmpty(transformTemplate))
{
settings.ReplaceSettings = new ProxyUrlReplaceSettings
{
TransformTemplate = transformTemplate,
TransformerType = parser.GetEnumValue($"{prefix}TransformerType", TransformerType.Handlebars)
};
}
}

Some files were not shown because too many files have changed in this diff Show More