GitHub
09/04/2025, 9:57 PMPactNet.Interop package as @adamrodger suggested in Issue #492 . This was not a breaking change since all the classes in this namespace were internal, PactNet now references this new package and none of the pre-existing interface methods have changed. Extracting this package made it possible to create bare-bones Grpc plugin tests and is effectively a very "raw" way of using any plugin. Perhaps a next step would be to wrap the bare-bones methods in a more fluent "raw" interface in a plugin package similar to implementations in other languages as @Matt (pactflow.io / pact-js / pact-go) suggested.
Based on above "raw" tests I was able to refactor and extract shared functionality to create a new PactNet.Extensions.Grpc library that provides a fluent interface similar to the interfaces for http / rest and asynchronous messaging interactions.
An alternative approach here would be to just add <InternalsVisibleTo Include="PactNet.Extensions.Grpc" /> to the PactNet.csproj file but this allows integration of other plugins in repos outside this one so it depends on whether you want to explicitly allow that or not.
pact-foundation/pact-netMartin
09/05/2025, 11:47 AMtext/plain instead of JSON.
What I’ve noticed:
• On the consumer side, the DSL only exposes helpers like WithJsonContent(...) which always serialize the messages to JSON. I don’t see a way to say "the body is raw text and the contentType is `text/plain`".
• On the provider side, when using .WithMessages(...).WithContent(...), any string gets serialized as JSON and the content type ends up application/json. I don’t see an overload that doesn't take the JsonSerializationOptions.
So essentially: I don’t see a way in PactNet to produce or verify a message pact where the body is plain text and the content type is text/plain.
👉 My questions:
• Is this a known limitation in PactNet?
• Or is there some sample code / workaround that shows how to handle non-JSON content types (like text/plain) in message pacts on .NET?
We’re using PactNet 5.0.1 (PactNet + PactNet.Abstractions).
Thanks in advance!GitHub
09/05/2025, 2:40 PMgit status you will see that PactNet.sln all line-endings have immediately changed. Potential solution is described in the accepted answer of the above referenced SO post.
Appears that the root cause may be that line endings were not renormalized (git add --renormalize .) when the .gitattributes file was added.
pact-foundation/pact-netPaul
09/12/2025, 5:17 AMElson Ho
09/19/2025, 2:31 AMGitHub
09/20/2025, 4:01 PM<https://github.com/pact-foundation/pact-net/tree/master|master> by adamrodger
<https://github.com/pact-foundation/pact-net/commit/8b8841d4074c17363fa22e62a105711e88eb24a4|8b8841d4> - Fix content creation syntax in messaging pacts
pact-foundation/pact-netGitHub
09/20/2025, 7:12 PM<https://github.com/pact-foundation/pact-net/tree/master|master> by adamrodger
<https://github.com/pact-foundation/pact-net/commit/3f59bf7cb00124635b6d421a033b2a4a72c4c3c9|3f59bf7c> - fix(#549): EditorConfig settings for sln files
pact-foundation/pact-netGitHub
09/30/2025, 10:43 AMGitHub
10/07/2025, 10:58 PMPactNet.Extensions.Grpc package I created in PR #548.
There may still be some design issues to consider. One thing to note is that Plugins can have both synchronous and asynchronous interactions so I made my builder specific to the synchronous use case so we can add an asynchronous version at some point.
pact-foundation/pact-netKieran McCarthy
10/11/2025, 10:49 AMGitHub
10/14/2025, 10:04 AMGitHub
10/15/2025, 5:09 AMGitHub
10/22/2025, 9:38 PMGitHub
10/22/2025, 9:43 PMDOTNET_NUGET_SIGNATURE_VERIFICATION to false
## Compatibility
### Operating System
Due to using a shared native library instead of C# for the main Pact logic only certain OSs are supported:
| OS | Arch | Support |
| ------------ | ----------- | ---------------- |
| Windows | x86 | ❌ No |
| Windows | x64 | ✔️ Yes |
| Linux (libc) | x86 | ❌ No |
| Linux (libc) | x64 | ✔️ Yes |
| Linux (musl) | x64 | ✔️ Yes (Tier 2)* |
| Linux (libc) | ARM | ✔️ Yes (Tier 3)* |
| Linux (musl) | ARM | ✔️ Yes (Tier 3)* |
| OSX | x64 | ✔️ Yes |
| OSX | ARM (M1/M2) | ✔️ Yes |
#### Support
• Tier 1
• Established
• Full CI/CD support.
• Users should not encounter issues
• Full reproducible examples running in CI, should be provided by users raising issues
• If using musl targets, users should attempt the same test on a libc target (such as debian)
• Tier 2
• Recently introduced
• Full CI/CD support.
• Users may encounter issues
• Full reproducible examples running in CI, should be provided by users raising issues
• If using musl targets, users should attempt the same test on a libc target (such as debian)
• Tier 3
• Recently introduced, No/limited CI/CD support.
• Users may encounter issues
• Full reproducible examples which can be run by maintainers locally, should be provided by users raising issues
pact-foundation/pact-netGitHub
10/22/2025, 9:44 PMGitHub
11/10/2025, 5:35 AMGitHub
11/10/2025, 5:55 AM<https://github.com/pact-foundation/pact-net/tree/master|master> by JP-Ellis
<https://github.com/pact-foundation/pact-net/commit/6c88659ee367f3fab7a269e876166d885cdc79d3|6c88659e> - chore: fix json schema url
pact-foundation/pact-netGitHub
12/28/2025, 10:49 PMprovidertests code to generate OrderCreatedEvent with any number other than 1
Expected behavior
Pact Verify test should fail with body content mismatch
Log Output
ated"}: pact_verifier::messages: actual message = AsynchronousMessage { id: None, key: None, description: "Asynchronous/Message Interaction", provider_states: [], contents: MessageContents { contents: Present(b"{\"id\":30}", Some(ContentType { main_type: "application", sub_type: "json", attributes: {}, suffix: None }), None), metadata: {"contentType": String("application/json")}, matching_rules: MatchingRules { rules: {} }, generators: Generators { categories: {} } }, comments: {}, pending: false, plugin_config: {}, interaction_markup: InteractionMarkup { markup: "", markup_type: "" }, transport: None }
2025-12-28T21:57:19.815008Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: comparing to expected message: AsynchronousMessage { id: None, key: None, description: "an event indicating that an order has been created", provider_states: [], contents: MessageContents { contents: Present(b"{\"id\":35}", Some(ContentType { main_type: "application", sub_type: "json", attributes: {}, suffix: None }), None), metadata: {}, matching_rules: MatchingRules { rules: {BODY: MatchingRuleCategory { name: BODY, rules: {DocPath { path_tokens: [Root, Field("id")], expr: "$.id" }: RuleList { rules: [Integer], rule_logic: And, cascaded: false }} }} }, generators: Generators { categories: {} } }, comments: {}, pending: false, plugin_config: {}, interaction_markup: InteractionMarkup { markup: "", markup_type: "" }, transport: None }
2025-12-28T21:57:19.815059Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: expected content type = 'application/json', actual content type = 'application/json'
2025-12-28T21:57:19.815069Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_plugin_driver::catalogue_manager: Looking for a content matcher for application/json
2025-12-28T21:57:19.815900Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: No content matcher defined for content type 'application/json', using core matcher implementation
2025-12-28T21:57:19.815909Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: Using body matcher for content type 'application/json'
2025-12-28T21:57:19.815917Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: compare: Comparing path $
2025-12-28T21:57:19.815919Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: compare_maps: Comparing maps at $: {"id": Number(35)} -> {"id": Number(30)}
2025-12-28T21:57:19.815944Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: compare: Comparing path $.id
2025-12-28T21:57:19.815949Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: compare_values: Calling match_values for path $.id
2025-12-28T21:57:19.815953Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: JSON -> JSON: Comparing '35' (Integer) to '30' (Integer) using Integer -> Ok(())
2025-12-28T21:57:19.815957Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching::json: compare_values: Comparing 'Number(35)' to 'Number(30)' at path '$.id' -> Ok(())
2025-12-28T21:57:19.815975Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: Matching message metadata
2025-12-28T21:57:19.815977Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: Matching message metadata. Expected '{"contentType": String("application/json")}', Actual '{"contentType": String("application/json")}'
2025-12-28T21:57:19.815982Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: Comparing metadata values for key 'contentType'
2025-12-28T21:57:19.815989Z DEBUG ThreadId(01) verify_interaction{interaction="an event indicating that an order has been created"}: pact_matching: Comparing message context type '"application/json"' => '"application/json"'
Additional context
Suspicious text
compare_values: Comparing 'Number(35)' to 'Number(30)' at path '$.id' -> Ok(())
Fulfilment API-Orders API.json excerpt of contents
{
"contents": {
"content": {
"id": 35
},
"contentType": "application/json",
"encoded": false
},
"description": "an event indicating that an order has been created",
"matchingRules": {
"body": {
"$.id": {
"combine": "AND",
"matchers": [
{
"match": "integer"
}
]
}
}
},
"pending": false,
"type": "Asynchronous/Messages"
}
pact-foundation/pact-netPaul Williams
03/02/2026, 8:38 AMNorbert Gulácsi
03/25/2026, 12:17 PMVaibhav Suryawanshi
04/14/2026, 5:18 AMVaibhav Suryawanshi
04/14/2026, 9:32 AMMatt (pactflow.io / pact-js / pact-go)
GitHub
06/08/2026, 2:23 PMGitHub
07/13/2026, 8:05 AM2025-07-10T21:23:11.928119Z ERROR ThreadId(01) pact_verifier::pact_broker: Failed to push branch refs/head/test for provider version test
2025-07-10T21:23:11.928223Z ERROR ThreadId(01) pact_verifier: Publishing of verification results failed with an error: Link/Resource was not found - Request to pact broker path '/pacticipants/myprovider/branches/refs%2Fhead%2Ftest/versions/test' failed: 404 Not Found. URL: '<https://pactbroker.com/>'
Steps To Reproduce
options.PublishResults(brokerConfig.ProviderVersion, publishOptions =>
{
publishOptions
.BuildUri("<http://test>")
.ProviderBranch("/refs/head/test");
});
Expected behavior
The provider verification result should be published to a branch with a slash. This is a supported value with the pact cli to publish consumer contract.
Log Output
See above
Additional context
Replacing / with - make it works.
pact-foundation/pact-netGitHub
07/21/2026, 12:37 AMGitHub
07/31/2026, 2:20 PMcomments.references.AsyncAPI structure.
According to PactFlow's AsyncAPI BDCT documentation, consumer pact files should include operation references like this:
{
"description": "Message interaction",
"comments": {
"references": {
"AsyncAPI": {
"operationId": "receiveUserEvents"
}
}
}
}
Current State in PactNet
PactNet 5.0.1 provides IMessageBuilderV4.WithMetadata() but does not expose a .reference() method to add AsyncAPI operation references.
Workaround (Verified Working)
I manually added the comments.references.AsyncAPI structure to the generated pact file:
{
"interactions": [
{
"description": "Some interaction",
"comments": {
"references": {
"AsyncAPI": {
"operationId": "someOperation"
}
}
},
"metadata": { ... },
"contents": { ... }
}
]
}
After adding this structure manually, PactFlow successfully validated the Message Pact against the AsyncAPI provider specification. This confirms that the feature works end-to-end — PactNet just needs a programmatic way to add these references.
Expected Behavior
PactNet should support something like this:
_pactBuilder
.ExpectsToReceive("Some message")
.Reference("AsyncAPI", "operationId", "someOperation") // ← Missing
.WithJsonContent(new { ... })
.Verify<Message>(...)
Or alternatively:
_pactBuilder
.ExpectsToReceive("Some message")
.WithAsyncApiReference("someOperation") // ← Missing
.WithJsonContent(new { ... })
.Verify<Message>(...)
Use Case
This feature is essential for teams using PactFlow's AsyncAPI BDCT approach with .NET Message Pact consumers. Without it, the generated pact files cannot be automatically validated against AsyncAPI provider specifications in Pactflow.
Environment
• PactNet Version: 5.0.1
• .NET Version: 10.0
• Target: PactFlow AsyncAPI BDCT
Thanks in advance!
pact-foundation/pact-netGitHub
08/04/2026, 6:24 PM5.0.0 and 5.0.1
• Pact Specification: V3 (messages) and V4 (asyncMessage)
• Framework: .NET 8.0 / .NET 10
• OS: Cross-platform (macOS, Windows, Linux)
---
Describe the bug
When performing provider verification for an asynchronous message contract using the new unified PactVerifier.WithMessages(...) builder introduced in PactNet 5.0+, the provider verification instantly fails before executing any message scenario or mapper.
The underlying Rust verification engine attempts to send HTTP requests to a URL with the scheme message:// instead of http://, which causes the internal Rust HTTP client (reqwest) to fail immediately with a URL builder error: builder error for url (<message://localhost:XXXX/pact-messages>).
---
Expected behavior
PactNet starts a tiny local HTTP server within the test process (`http://localhost:PORT/pact-messages/`) to host the messaging scenarios. When instructing the native Rust engine to verify the interactions, the verification engine should send POST requests via HTTP to that internal server (`http://localhost:PORT/pact-messages/`), receive the serialized message, and compare fields against the contract.
---
Actual behavior & Contradictory Logs
The verifier logs reveal the exact contradiction in two sequential lines:
Starting messaging provider at <http://localhost:49152/pact-messages/> <-- PactNet starts HTTP adapter correctly
Sending request to provider at <message://localhost:49152/pact-messages/> <-- Rust FFI engine requests wrong scheme
Request Failed - builder error for url (<message://localhost:49152/pact-messages>)
Because reqwest does not recognize message:// as a valid network transport scheme, the HTTP request never leaves the builder phase, failing with zero field comparisons performed.
Root Cause Analysis
In PactNet 5.0+, when registering the message transport provider via FFI (likely invoking pactffi_verifier_add_provider_transport), two distinct attributes are handled:
protocol: The interaction type (message).
scheme: The network scheme to physically reach the local verification adapter (http).
It appears PactNet correctly registers the protocol as "message", but either leaves the scheme undefined or erroneously passes "message" as the scheme as well. Consequently, the native engine defaults to duplicating the protocol name into the URL scheme, building message://localhost:49152/pact-messages/ instead of http://....
Steps to Reproduce
Create a consumer message test generating a valid message pact (tested with both V3 and V4 specifications).
Set up a provider test using PactVerifier:
csharp
using var verifier = new PactVerifier("MyProvider", config);
verifier
.WithMessages(scenarios => scenarios.Add("a sample message", () => MyMessageFactory.Create()), jsonOptions)
.WithFileSource(pactFilePath)
.Verify();
Run the provider test. Notice that none of the scenario factory methods are invoked, and verification terminates immediately with builder error for url (message://...).
Why this is blocking & tested combinations
We ran systematic testing across multiple matrix combinations to rule out local configuration issues:
PactNet Versions: Fails identically on 5.0.0 and 5.0.1.
Contract Formats: Fails identically on both V3 and V4 specs.
Data/Mapper execution: Confirmed via debugging that scenarios and mappers never get called because the internal POST request fails during URL construction.
Configuration: PactVerifierConfig does not expose any public knobs to manually override the scheme for messaging transports.
pact-foundation/pact-netGitHub
08/10/2026, 11:15 PM<https://github.com/pact-foundation/pact-net/tree/master|master> by JP-Ellis
<https://github.com/pact-foundation/pact-net/commit/519d86279daba984b78b2cea1d9da453a7d661b6|519d8627> - ci: remove unused triage board workflow
pact-foundation/pact-netGitHub
08/13/2026, 2:53 PMWithComment(key, value) - for custom key-value comments
• WithTextComment(comment) - for text comments array
• WithAsyncApiReference(operationId) - convenience method for AsyncAPI refs
Changes
• Core implementation in MessageBuilder and drivers
• FFI bindings for pactffi_set_comment and pactffi_add_text_comment
• Unit, integration, and E2E tests
• Updated sample code and documentation
• V4 only (V3 doesn't support comments per spec)
Testing
All 411 tests passing. Added comprehensive test coverage.
Breaking Changes
None - additive change to IMessageBuilderV4 only.
pact-foundation/pact-net