Contract Testing in .NET — PactNet Enum Removal Failures
A silent enum removal broke PayPal payments across services.
20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- Consumer-Driven Contracts (CDC) let each consumer define its API expectations as a pact file
- PactNet generates pact files from consumer-side tests, then provider verifies them independently
- Provider states simulate exact conditions (e.g. 'user exists') during verification
- A pact mismatch fails the provider build, not the consumer — breaking the cycle early
- Biggest mistake: treating pacts as static specs instead of versioned, evolving contracts
- Key rule: pact files must be shared via a broker or repo — never email or wiki
Imagine you order a custom-made key from a locksmith to fit your front door. The locksmith makes the key based on a mould (the contract) you gave them — so when you pick it up, it fits perfectly without you needing to be there while they cut it. Contract testing works the same way: the team who uses an API writes down exactly what they expect it to return (the contract), and the team who owns the API runs tests against that contract independently. No more 'it worked on my machine' surprises when services talk to each other.
In a microservices architecture, the scariest failures aren't the ones your unit tests catch — they're the silent ones that only blow up in production when Service A calls Service B and gets back a response shape it never expected. A field gets renamed, a nullable becomes required, an enum value disappears. Your 2000-unit-test suite is all green. Your integration environment is down for maintenance. And at 2am, your on-call phone rings. Contract testing exists precisely to close this gap.
The problem is subtle: traditional integration tests force you to spin up every dependent service simultaneously, coordinate deployment windows, and pray the test data lines up. That's slow, brittle, and it couples your CI pipelines together in ways that feel manageable on day one and catastrophic at scale. Consumer-Driven Contract (CDC) testing flips the model — each consumer declares what it needs, each provider proves it can satisfy those needs, and they do it asynchronously, independently, and fast.
By the end of this article you'll know how to implement full consumer-driven contract testing in .NET using PactNet, understand the internals of how pact files are generated and verified, handle edge cases like optional fields and provider states, integrate pact verification into your CI/CD pipeline, and avoid the production gotchas that catch even experienced teams off guard.
What is Contract Testing in .NET?
Contract testing in .NET is about defining a formal agreement between a service consumer (e.g., an API client) and a provider (e.g., a REST API). The agreement, called a pact, describes the exact request the consumer will make and the exact response it expects. With PactNet—the .NET implementation of the Pact framework—you write these expectations as unit tests in the consumer project.
The key insight: the consumer's tests generate a JSON file (the pact). The provider then runs its own tests that replay those expectations against the real provider server. No need to deploy both services together, no fragile integration environment.
Let's see a concrete example. Suppose we have an Orders API that returns order details. The consumer (a web frontend) expects a GET /orders/1 to return a 200 with body { id: 1, status: "shipped" }. The consumer test in PactNet builds that expectation.
Consumer-Driven Contract Testing with PactNet
The consumer-driven approach means the consumer dictates what it needs from the API. The provider doesn't decide the contract unilaterally. This sounds backward, but it's the only way to catch breaking changes before they reach production.
In practice, the consumer team writes PactNet tests that define every interaction they rely on. For each interaction, they specify: - The HTTP method and path - Query parameters, headers, and body (if any) - Expected response status, headers, and body structure - Provider states — named conditions the provider must set up (e.g., "order exists")
Once all consumer tests pass, the pact file is published to a Pact Broker (or stored in a shared artifact repository). The provider's CI pipeline then runs verification tests against that pact. If the provider breaks a contract, the provider build fails — not the consumer's. This shifts the failure left, before the change is ever deployed.
- The consumer defines what the API should do (like a specification).
- The provider must implement to that specification — not the other way around.
- If the provider changes the API, the consumer's pact will catch it in the provider's CI.
- This inverts the usual testing flow: the downstream team protects itself.
Provider Verification with PactNet
Now the provider team sets up verification tests. They don't need to write test scenarios — they simply load the pact file and replay it against a running instance of their API. PactNet's verification engine calls each endpoint with the exact request from the pact and checks the response matches expectations.
The provider must register provider states — methods that set up the exact data conditions required by the consumer tests. For example, if the consumer expects "order 1 exists", the provider must have a method that inserts an order with ID 1 into the test database.
Here's how to write provider verification in .NET:
Handling Provider States and Edge Cases
Provider states are the trickiest part of PactNet. They let the consumer describe preconditions without knowing how the provider sets them up. Common edge cases include:
- Missing state: the consumer says "order exists" but the provider hasn't registered that state. Verification fails.
- State with dynamic data: the consumer expects a specific ID, but the provider's test data generates random IDs. The consumer must use a fixed ID.
- Multiple states per interaction: some interactions need multiple conditions (e.g., "user is logged in" AND "order exists"). PactNet supports combining states as an array.
- Teardown: after each state, the provider should clean up any data to avoid test pollution. Use teardown endpoint.
Here's how to handle a consumer test that uses multiple states:
CI/CD Integration and Production Gotchas
The real value of contract testing comes from integrating it into your CI/CD pipelines. Both consumer and provider pipelines must run pact tests on every pull request. The typical flow:
- Consumer PR: run pact tests, publish pact file to broker.
- Provider PR: fetch latest pacts from broker, verify all interactions.
- Provider can only merge if all pacts pass.
- Consumer can deploy independently — the broker tells them which provider versions are safe.
PactNet supports publishing to a Pact Broker. The broker stores all versions of all pacts and can verify compatibility matrix.
- Missing broker authentication: use API tokens, never hardcode secrets.
- Pact file versioning: always tag pacts with the consumer version (git commit SHA). The broker uses version tags to maintain history.
- Race condition in CI: if consumer publishes pact after provider starts verification, provider might use stale pact. Use webhooks or scheduled verification.
- Pact file not published: consumer test may pass locally but publishing step may be missing in CI.
- Every pact version is tagged with consumer version, branch, and environment.
- Provider verification results are recorded against each pact version.
- You can query the broker: 'Which version of OrderApi is verified against WebFrontend v1.0?'
- This enables safe deployment — deploy only when compatible versions exist.
When Pact Verification Fails: Real Debugging Patterns
Even with well-written pacts, verification failures happen. Here's how to debug the most common causes:
1. Response field mismatch: PactNet compares actual response against expected using structural tolerance by default. If a field is missing or has wrong type, the verifier reports the exact path. Check the verifier log at debug level for details.
2. Dynamic fields: If your API returns timestamps or GUIDs, the pact must use a matcher (like PactNet's Match.Type or Match.Regex) to say 'I expect a string of this pattern, not an exact value'.
3. Query parameter order: Some HTTP clients order query parameters alphabetically. If your consumer sends them in a different order, the pact request will differ. Use List matching or ignore order.
4. Provider state setup failure: If the provider state method throws an exception, the verification stops. Check that the state name matches exactly (case-sensitive) and that all dependencies (e.g., test database) are available.
5. Missing headers: PactNet expects exact header values. If your provider adds additional headers (like Server or X-Request-Id), the verifier will fail. Use 'header blacklist' or match headers loosely.
Why You Should Treat Pact Files Like Database Migrations
Pact files are not artifacts you casually regenerate. They are contracts that represent a binding agreement between consumer and provider. Treat them like database migrations: version-controlled, reviewed, and immutable after agreement. If you regenerate a pact file silently, you break your provider's verification without any trace. That leads to production incidents where the provider passes against stale expectations while the consumer sends data the provider rejects. The fix: store pact files in a separate repository or a dedicated branch. Consumer merges only after provider verifies. Use PactFlow or a simple git hook to enforce that no pact file changes without a corresponding provider verification run. Your CI pipeline should fail if a provider cannot verify the latest consumer pact. This is not overengineering. This is preventing the "works on my machine" scenario from reaching staging. Senior teams enforce this because they've been burned by silent contract drift.
Pact Brokers Are Not Optional — Here's the Bare Minimum Setup
Developers love skipping the infrastructure. They run pact verification locally with hardcoded files and call it done. That works until you have three consumers hitting one provider. Suddenly you need to know: which version of the contract did the mobile team publish? Did the web consumer break anything? That's why a Pact Broker exists. It's not a luxury. It's the source of truth for contract versions. Without it, you're guessing. The minimal setup: deploy Pact Broker (open-source on Docker) or use PactFlow (paid, but saves headaches). Store pact files there after consumer tests pass. Provider CI pulls the latest pacts from the broker, not from consumer repos. Consumer CI tags pacts with git commit hashes. Provider CI compares against those tags. This gives you a timeline of contract changes and who broke what. Setup time: two hours. Time saved on debugging: every production incident you prevent.
Pact Provider Verification with PactNet in .NET
Provider verification is the process of validating that a provider API satisfies all consumer expectations defined in a Pact file. In PactNet, this is accomplished using the PactVerifier class, which replays each interaction against the provider and checks that the actual response matches the expected one. To set up provider verification, install the PactNet NuGet package and create a verification test. The following example demonstrates verifying a simple API endpoint that returns a user by ID. The test uses PactVerifier with a broker URL (or local file) and specifies provider states to handle data setup. The verification runs in a test host, typically using Microsoft.AspNetCore.TestHost to spin up the provider in-memory. This approach ensures fast, isolated tests that catch contract violations early. Note that provider verification should be part of your CI pipeline, triggered on changes to either the provider or consumer code. PactNet supports both synchronous HTTP and asynchronous message interactions, but for HTTP APIs, the setup is straightforward. The key is to match the provider state exactly as defined in the consumer test to avoid false failures. Below is a complete example of a provider verification test using PactNet with a local Pact file.
Consumer-Driven Contract Testing Workflow
The consumer-driven contract testing workflow starts with the consumer team defining expectations for the provider API. In PactNet, this is done by writing a consumer test that uses PactBuilder to set up interactions, then calling the provider endpoint via a mock service. The test generates a Pact file (JSON) that captures all interactions. This file is then shared with the provider team, typically via a Pact Broker or a shared repository. The provider team runs verification tests using PactVerifier to ensure their API matches the Pact file. If verification fails, the provider must fix the API or coordinate with the consumer to update the contract. The workflow is iterative: as the consumer adds new requirements, they update the Pact file and re-run verification. This end-to-end example demonstrates a consumer test for a GET /users/{id} endpoint. The consumer defines an interaction expecting a 200 response with a specific user object. After the test passes, the Pact file is published to a broker. The provider then runs verification against the same Pact file. This workflow catches mismatches early, before integration testing, and reduces the risk of breaking changes. Below is a complete consumer test example.
Contract Testing vs Integration Testing vs End-to-End Testing
Contract testing, integration testing, and end-to-end (E2E) testing serve different purposes in a testing strategy. Contract testing focuses on verifying that two services (consumer and provider) agree on the API contract. It uses isolated, fast tests that mock the provider (consumer side) or replay interactions (provider side). Integration testing validates that multiple components work together correctly, often involving real databases, queues, or external services. It is slower and more brittle than contract testing. E2E testing tests the entire system from the user interface to backend services, simulating real user scenarios. It is the slowest and most expensive, but provides the highest confidence. Contract testing fills the gap between unit tests and integration tests by catching API mismatches early. It is not a replacement for integration or E2E tests, but a complement. For example, contract testing ensures that the consumer's request format matches the provider's expectations, while integration testing might verify that the provider correctly queries a database. E2E testing then validates that the whole flow works. In practice, use contract testing for service-to-service interactions, integration testing for internal component interactions, and E2E sparingly for critical user journeys. Below is a comparison table in code comments.
The Silent Enum Removal That Brought Down Payments
- A contract must be machine-readable and verified every deployment.
- Never rely on documentation or manual communication for API changes.
- Every consumer must declare exactly what it expects, including enum values, optional fields, and response codes.
dotnet test --filter "Category=Verification" --verbosity normalCheck PactNet logs at Debug level: set PACT_VERBOSE=true in environment| File | Command / Code | Purpose |
|---|---|---|
| Consumer | using PactNet; | What is Contract Testing in .NET? |
| Consumer | public class OrderApiConsumerTests | Consumer-Driven Contract Testing with PactNet |
| Provider | using PactNet.Verifier; | Provider Verification with PactNet |
| Consumer | _pactBuilder | Handling Provider States and Edge Cases |
| Consumer | var publisher = new PactPublisher("https://your-pact-broker.com", | CI/CD Integration and Production Gotchas |
| Consumer | using PactNet.Matchers; | When Pact Verification Fails |
| PactAsMigration.cs | public class PactMigrationGate | Why You Should Treat Pact Files Like Database Migrations |
| PactBrokerSetup.cs | using PactNet.Verifier; | Pact Brokers Are Not Optional |
| ProviderVerificationTests.cs | using PactNet; | Pact Provider Verification with PactNet in .NET |
| ConsumerTests.cs | using PactNet; | Consumer-Driven Contract Testing Workflow |
Key takeaways
Interview Questions on This Topic
How does PactNet differ from traditional integration testing for microservices?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.
That's Testing. Mark it forged?
7 min read · try the examples if you haven't