Home C# / .NET Contract Testing in .NET — PactNet Enum Removal Failures
Advanced 7 min · March 06, 2026

Contract Testing in .NET — PactNet Enum Removal Failures

A silent enum removal broke PayPal payments across services.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
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.

Imagine you order a custom-made key from a locksmith to fit your front door.

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.

Plain-English First

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/OrderApiConsumerTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using PactNet;
using PactNet.Infrastructure.Outputters;
using Xunit;

public class OrderApiConsumerTests
{
    private readonly IPactBuilderV3 _pactBuilder;

    public OrderApiConsumerTests()
    {
        var pactConfig = new PactConfig
        {
            PactDir = @"..\..\pacts",
            Outputters = new[] { new ConsoleOutput() }
        };
        _pactBuilder = PactNet.Pact.V3(
            "WebFrontend",   // consumer name
            "OrderApi",      // provider name
            pactConfig);
    }

    [Fact]
    public async Task GetOrder_ReturnsOrder_WhenOrderExists()
    {
        _pactBuilder
            .UponReceiving("a GET request for order 1")
                .Given("order 1 exists")
                .WithRequest(HttpMethod.Get, "/orders/1")
                .WithHeader("Accept", "application/json")
            .WillRespond()
                .WithStatus(HttpStatusCode.OK)
                .WithHeader("Content-Type", "application/json")
                .WithJsonBody(new
                {
                    id = 1,
                    status = "shipped"
                });

        await _pactBuilder.VerifyAsync(async ctx =>
        {
            // Real consumer code that calls the mock server
            var client = new HttpClient { BaseAddress = ctx.MockServerUri };
            var response = await client.GetAsync("/orders/1");
            var body = await response.Content.ReadAsStringAsync();
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            Assert.Contains("\"status\": \"shipped\"", body);
        });
    }
}
Output
Test run generates pact file at ../pacts/WebFrontend-OrderApi.json
💡PactNet naming conventions
Consumer and provider names must be unique across your organisation. If you have multiple consumers of the same API, each will generate its own pact file. The provider will verify all of them.
📊 Production Insight
If the consumer and provider names change between branches, the broker treats them as separate pacts.
This causes 'orphan' pacts that never get verified.
Rule: use consistent naming across all environments — consumer name = service name, not branch name.
🎯 Key Takeaway
A pact file is a contract generated by consumer tests.
Provider tests verify the contract against real provider code.
The pact is the single source of truth — never modify it by hand.
contract-testing-dotnet PactNet Contract Testing Stack Layered architecture for consumer-driven contracts Consumer Application API Client | PactNet Test Fixture Contract Generation Pact File (JSON) | Pact Broker Publisher Pact Broker Contract Storage | Version Tags | Webhook Triggers Provider Verification PactNet Verifier | Provider State Setup | Mock Provider CI/CD Pipeline Verification Gate | Deployment Approval THECODEFORGE.IO
thecodeforge.io
Contract Testing Dotnet

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.

Consumer/OrderApiConsumerTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class OrderApiConsumerTests
{
    // ... setup as before ...

    [Fact]
    public async Task CreateOrder_Returns201_WhenValidRequest()
    {
        _pactBuilder
            .UponReceiving("a POST request to create order")
                .Given("the system is ready to accept orders")
                .WithRequest(HttpMethod.Post, "/orders")
                .WithHeader("Content-Type", "application/json")
                .WithJsonBody(new
                {
                    productId = 42,
                    quantity = 2
                })
            .WillRespond()
                .WithStatus(HttpStatusCode.Created)
                .WithHeader("Location", "/orders/500");

        await _pactBuilder.VerifyAsync(async ctx =>
        {
            var client = new HttpClient { BaseAddress = ctx.MockServerUri };
            var response = await client.PostAsJsonAsync("/orders", new { productId = 42, quantity = 2 });
            Assert.Equal(HttpStatusCode.Created, response.StatusCode);
            Assert.Contains("/orders/", response.Headers.Location.ToString());
        });
    }
}
Output
Consumer tests pass. Pact file now contains two interactions: GET and POST.
Mental Model
Contracts as Reverse Tests
Think of consumer tests as writing the test first, but for an API you don't control.
  • 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.
📊 Production Insight
Consumer tests that use random data (e.g., a new ID each run) will break pact verification because the provider state needs a deterministic ID.
Solution: use fixed test data in consumer tests and match it in provider states.
Rule: every pact interaction must have a deterministic provider state.
🎯 Key Takeaway
Consumer drives the contract.
Provider verifies it.
Pact file is the handshake — version it like any other artifact.

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.

Provider/OrderApiProviderTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using PactNet.Verifier;
using Xunit;

public class OrderApiProviderTests : IClassFixture<OrderApiFixture>
{
    private readonly OrderApiFixture _fixture;

    public OrderApiProviderTests(OrderApiFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void VerifyPacts()
    {
        var config = new PactVerifierConfig
        {
            Outputters = new[] { new ConsoleOutput() },
            LogLevel = PactNet.LogLevel.Debug
        };

        var verifier = new PactVerifier(config);

        verifier
            .ServiceProvider("OrderApi", _fixture.ServerUri)
            .HonoursPactWith("WebFrontend")
            .PactUri(@"..\..\pacts\WebFrontend-OrderApi.json")
            .WithProviderStateUrl(new Uri(_fixture.ServerUri, "/provider-states"))
            .Verify();
    }
}

// Provider states controller (run in-memory with WebApplicationFactory)
[ApiController]
[Route("provider-states")]
public class ProviderStatesController : ControllerBase
{
    [HttpPost("setup")]
    public IActionResult Setup([FromBody] ProviderState state)
    {
        if (state.State == "order 1 exists")
        {
            // Arrange: insert order with id=1 into test database
            TestData.Orders.Add(new Order { Id = 1, Status = "shipped" });
        }
        return Ok();
    }

    [HttpPost("teardown")]
    public IActionResult Teardown()
    {
        TestData.Orders.Clear();
        return Ok();
    }
}
Output
PactNet replays each interaction against the real API and reports any mismatches.
⚠ Provider State Overhead
Provider states add test setup complexity. Limit them to only what the consumer truly needs. Over-specific states (e.g., 'order exists with status shipped and total > 100 and discount applied') will make the provider test suite brittle and slow.
📊 Production Insight
In CI, the provider must start a real instance of the API (with test database) before running verification.
Use WebApplicationFactory from ASP.NET Core to spin up a lightweight in-memory server.
Rule: never verify against a shared integration environment — state pollution will cause false negatives.
🎯 Key Takeaway
Provider verification replays pacts against real provider code.
Provider states are the glue between consumer expectations and test data.
Use in-memory test servers for fast, isolated verification.
contract-testing-dotnet PactNet Enum Handling: Removal vs Deprecation Comparing strategies for enum value changes in contracts Enum Removal Enum Deprecation Contract Compatibility Breaks existing contracts Maintains backward compatibility Consumer Impact Immediate consumer failures Graceful migration period Provider Effort Simple code removal Requires deprecation logic Testing Complexity PactNet fails on missing enum PactNet passes with deprecation CI/CD Risk High risk of broken pipelines Low risk with phased rollout THECODEFORGE.IO
thecodeforge.io
Contract Testing Dotnet

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.
Consumer/OrderApiConsumerTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Consumer test with multiple provider states
_pactBuilder
    .UponReceiving("a GET request for pending orders by user")
        .Given(new[] { "user exists", "user has pending orders" })
        .WithRequest(HttpMethod.Get, "/orders/pending")
        .WithQuery("userId", "42")
    .WillRespond()
        .WithStatus(HttpStatusCode.OK)
        .WithJsonBody(new[]
        {
            new { id = 1, status = "pending" },
            new { id = 2, status = "pending" }
        });

// Provider verification setup
public class ProviderStates
{
    [ProviderState("user exists")]
    public static void UserExists() => TestData.Users.Add(new User { Id = 42, Name = "Alice" });

    [ProviderState("user has pending orders")]
    public static void UserHasPendingOrders()
    {
        TestData.Orders.AddRange(new[]
        {
            new Order { Id = 1, UserId = 42, Status = "pending" },
            new Order { Id = 2, UserId = 42, Status = "pending" }
        });
    }
}
Output
Both provider states are called in sequence, then the request is made.
🔥State naming convention
Use names that describe the observable condition, not the setup steps. 'user exists' is good. 'insert user into database' is bad because it couples consumer to provider's implementation.
📊 Production Insight
Provider states that fail to clean up after themselves cause test pollution across pact interactions.
In CI with parallel verification, shared state corrupts unrelated tests.
Rule: always wipe test data between states — use teardown endpoint or TransactionScope rollback.
🎯 Key Takeaway
Provider states declare preconditions for each interaction.
They must be deterministic and idempotent.
Teardown is as important as setup — avoid state pollution.
When to add a provider state
IfConsumer needs data that is not present by default
UseCreate a provider state that sets up that exact data
IfConsumer only needs default data (e.g., empty list)
UseNo state needed — provider should return default response
IfState setup is expensive (e.g., populating 1000 records)
UseReconsider if the consumer really needs that exact data; use simpler state

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:

  1. Consumer PR: run pact tests, publish pact file to broker.
  2. Provider PR: fetch latest pacts from broker, verify all interactions.
  3. Provider can only merge if all pacts pass.
  4. 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.

Common production gotchas
  • 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.
Consumer/PactPublisherSetup.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
// In consumer test setup, after all tests pass
var publisher = new PactPublisher("https://your-pact-broker.com",
    new PactUriOptions("broker-token-here"));
await publisher.PublishToBroker(
    pactFilePath,
    new Version(1, 0, 0),        // consumer version
    new Dictionary<string, string>
    {
        ["branch"] = "main",
        ["buildUrl"] = buildUrl
    });
Output
Pact file published to broker with version and tags. Provider CI will fetch it.
Mental Model
Pact Broker as Source of Truth
The broker is not just storage — it's a compatibility database.
  • 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.
📊 Production Insight
A common CI mistake: running provider verification against a pact file that was published 30 seconds earlier but the CI node fetches a cached old version.
Solution: always specify pact version tag or use broker's webhook to trigger provider verification.
Rule: never verify against latest — pin to a version tag (e.g., consumer branch name).
🎯 Key Takeaway
Integrate pact tests at PR gate, not after merge.
Use Pact Broker for versioned storage and compatibility matrix.
Tag pacts with consumer version — never rely on 'latest'.

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.

Consumer/OrderApiConsumerTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Using matchers for dynamic content
using PactNet.Matchers;

_pactBuilder
    .UponReceiving("a GET request for order 1")
        .Given("order 1 exists")
        .WithRequest(HttpMethod.Get, "/orders/1")
    .WillRespond()
        .WithStatus(HttpStatusCode.OK)
        .WithJsonBody(new
        {
            id = Match.Type(1),              // integer, exact value not important
            status = Match.Regex("shipped", "^(pending|shipped|cancelled)$"),
            created_at = Match.Type("2026-01-01T00:00:00") // datetime string
        });
Output
Verification uses matchers — actual response can have different values, as long as types and regex patterns match.
⚠ Overuse of matchers weakens contracts
Matchers are powerful but can make your contract too loose. Use them only for truly dynamic data (timestamps, IDs). For enums, docodes, and status, use exact values or Match.Regex with a strict pattern.
📊 Production Insight
The most common production-breaking pact failure is a field type change (e.g., 'userId' from int to string).
PactNet spots this, but only if the consumer used strict matching.
Rule: use matchers sparingly — prefer exact values for fixed fields.
🎯 Key Takeaway
Debug pact failures by checking exact field names, types, and ordering.
Use matchers only for dynamic data.
Provider state errors are usually naming mismatches.
Verification failure diagnosis
IfMismatch says 'field missing' but field is in response
UseCheck field name spelling — case sensitivity? Underscore vs camelCase?
IfMismatch says 'type mismatch'
UseCheck if consumer used Match.Type correctly. Actual may be string, pact expects integer.
IfProvider state not found
UseVerify state name exactly. Check for extra spaces or different casing.

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.

PactAsMigration.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — csharp tutorial

// Example: Enforcing pact verification before merge
// Use in your CI pipeline check
public class PactMigrationGate
{
    public static bool CanMergeConsumer(
        string consumerPactPath,
        string providerVerificationUrl)
    {
        var pactFile = File.ReadAllText(consumerPactPath);
        using var client = new HttpClient();
        
        // POST pact to provider's verification endpoint
        var response = client.PostAsync(
            providerVerificationUrl,
            new StringContent(pactFile, Encoding.UTF8, "application/json"))
            .GetAwaiter().GetResult();

        return response.IsSuccessStatusCode;
    }
}

// Usage: if (!CanMergeConsumer(...)) -> block PR
Output
This gate returns false if provider verification fails. Use as a merge check in Azure DevOps or GitHub Actions.
⚠ Production Trap:
Silently regenerating a pact file after consumer changes is the number one cause of contract drift. Make pacts immutable in your main branch.
🎯 Key Takeaway
Pact files are immutable contracts. Version them like database schemas. Never regenerate without provider verification.

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.

PactBrokerSetup.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — csharp tutorial

// Minimal Pact Broker client setup for provider verification
using PactNet.Verifier;

var config = new PactVerifierConfig
{
    LogLevel = LogLevel.Information,
    PublishVerificationResults = true
};

using var verifier = new PactVerifier(config);

verifier
    .ServiceProvider("PaymentProvider", new Uri("http://localhost:5001"))
    .WithPactBroker(new Uri("http://localhost:9292"))
    .WithProviderStateEndpoint(new Uri("http://localhost:5001/provider-states"))
    .Verify();

// Output shows each consumer pact and pass/fail status
Output
Output:
Verifying 'UserService' pact... PASSED
Verifying 'OrderApi' pact... PASSED
Verifying 'MobileClient' pact... FAILED - see Mismatches section
Publishing results to broker at http://localhost:9292
🔥Senior Shortcut:
Use Docker Compose for Pact Broker + PostgreSQL. Start with that. It's production-ready for small teams and teaches you the flow before you pay for PactFlow.
🎯 Key Takeaway
A Pact Broker is the shared source of truth. Without it, contract testing degrades into manual file shuffling.

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.

ProviderVerificationTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using PactNet;
using PactNet.Infrastructure.Outputters;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using Xunit;

public class ProviderVerificationTests
{
    [Fact]
    public async Task VerifyProviderHonoursPactWithConsumer()
    {
        var pactVerifier = new PactVerifier("UserService", new PactVerifierConfig
        {
            Outputters = new List<IOutput> { new XUnitOutput() },
            LogLevel = PactLogLevel.Information
        });

        pactVerifier
            .ServiceProvider("User API", new Uri("http://localhost:5000"))
            .HonoursPactWith("Consumer")
            .PactUri(@"..\..\..\pacts\consumer-userservice.json")
            .ProviderState($"{AppContext.BaseDirectory}/provider_states")
            .Verify();
    }
}

public class XUnitOutput : IOutput
{
    private readonly ITestOutputHelper _output;
    public XUnitOutput(ITestOutputHelper output) => _output = output;
    public void WriteLine(string line) => _output.WriteLine(line);
}
🔥Provider State Handlers
📊 Production Insight
In production, run provider verification as part of your CI pipeline, ideally on every commit to the provider repository. Use a Pact Broker to share Pact files between consumer and provider teams, and configure webhooks to trigger verification automatically when a new Pact is published.
🎯 Key Takeaway
Provider verification with PactNet ensures that the provider API meets all consumer expectations by replaying Pact interactions against the actual provider implementation.

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.

ConsumerTests.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using PactNet;
using PactNet.Mocks.MockHttpService;
using PactNet.Mocks.MockHttpService.Models;
using Xunit;
using System.Net.Http;
using System.Threading.Tasks;

public class ConsumerTests
{
    private readonly IMockProviderService _mockProviderService;
    private readonly string _mockProviderServiceBaseUri;

    public ConsumerTests()
    {
        var pactBuilder = new PactBuilder(new PactConfig { PactDir = @"..\..\..\pacts" })
            .ServiceConsumer("Consumer")
            .HasPactWith("User API");

        _mockProviderService = pactBuilder.MockService(MockServerPort: 9222);
        _mockProviderServiceBaseUri = $"http://localhost:{_mockProviderService.Port}";
    }

    [Fact]
    public async Task GetUser_WhenUserExists_ReturnsUser()
    {
        _mockProviderService
            .Given("User with ID 1 exists")
            .UponReceiving("A GET request to retrieve a user")
            .With(new ProviderServiceRequest
            {
                Method = HttpVerb.Get,
                Path = "/users/1",
                Headers = new Dictionary<string, string> { { "Accept", "application/json" } }
            })
            .WillRespondWith(new ProviderServiceResponse
            {
                Status = 200,
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } },
                Body = new { id = 1, name = "John Doe" }
            });

        var httpClient = new HttpClient { BaseAddress = new Uri(_mockProviderServiceBaseUri) };
        var response = await httpClient.GetAsync("/users/1");
        Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("John Doe", content);

        _mockProviderService.VerifyInteractions();
    }
}
💡Pact File Management
📊 Production Insight
In a microservices environment, this workflow allows teams to move fast without breaking each other. Integrate Pact into your CI/CD pipeline: consumer tests generate Pacts on commit, provider verification runs on deploy, and the broker enforces contract compatibility.
🎯 Key Takeaway
The consumer-driven contract testing workflow enables independent development by defining expectations upfront and verifying them continuously, reducing integration issues.

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.

TestingComparison.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Contract Testing: Verifies API contract between consumer and provider
// - Fast, isolated, deterministic
// - Example: PactNet consumer test
// - Scope: Single interaction

// Integration Testing: Verifies multiple components work together
// - Slower, requires real dependencies (DB, queues)
// - Example: Test that UserController returns data from SQL Server
// - Scope: Subsystem

// End-to-End Testing: Verifies entire system from UI to backend
// - Slowest, most expensive, flaky
// - Example: Selenium test that creates a user and checks UI
// - Scope: Full system
⚠ Don't Replace, Complement
📊 Production Insight
In production, use contract tests as a gate in your CI pipeline to prevent breaking changes from being deployed. Combine with integration tests for critical paths and E2E tests for high-risk user journeys. This reduces feedback time and deployment risk.
🎯 Key Takeaway
Contract testing is a lightweight, fast method to verify service compatibility, complementing integration and E2E tests by catching contract violations early.
● Production incidentPOST-MORTEMseverity: high

The Silent Enum Removal That Brought Down Payments

Symptom
Payments service returned 500 for PayPal transactions; other payment methods worked fine.
Assumption
The billing team assumed PaymentMethod enum values were documented in an internal wiki and that consumers would be notified of changes. No automated contract check existed.
Root cause
Two teams used separate deployment pipelines with no shared contract verification. The billing API's removal of a previously documented enum value went undetected until production traffic hit it.
Fix
Implemented consumer-driven contracts with PactNet: the payments service generated a pact file listing exactly which PaymentMethod values it expected. The billing pipeline now verifies against that pact before deploying. Enum removal now fails the billing build immediately.
Key lesson
  • 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.
Production debug guideQuick symptom-to-action reference for PactNet failures5 entries
Symptom · 01
Consumer test passes but provider verification fails
Fix
Check if the provider's response matches the pact exactly. Common causes: extra fields, wrong types, missing headers. Use PactNet's verbose verification logs.
Symptom · 02
Pact file not found on broker
Fix
Verify the broker URL and API token in both consumer and provider config. Ensure consumer publishes after test run.
Symptom · 03
Provider state not found
Fix
Register all provider states in the provider's test class. The state name must match exactly (case-sensitive) the one used in consumer test.
Symptom · 04
Verification passes locally but fails in CI
Fix
Check environment-specific variables (database connection, provider URI). Use Pact's log level 'debug' to capture HTTP interactions.
Symptom · 05
Consumer test flaky due to provider state setup
Fix
Ensure provider states are idempotent and transactional. Use a setup/teardown pattern to clean test data after each state.
★ 5-Minute PactNet Debug Cheat SheetCommands and checks for the most common PactNet contract failures in production pipelines.
Verification fails with 'missing field'
Immediate action
Check the pact file (in broker UI or local) to see expected fields vs actual response
Commands
dotnet test --filter "Category=Verification" --verbosity normal
Check PactNet logs at Debug level: set PACT_VERBOSE=true in environment
Fix now
Add the missing field to the provider's response or update the consumer pact to not expect it
Provider state 'user_exists' not found+
Immediate action
Confirm the provider test class has a method decorated with [ProviderState("user_exists")]
Commands
Search for 'ProviderState' in provider test project: grep -r "ProviderState" .
Verify state name in consumer test matches exactly (case and spaces)
Fix now
Add the missing provider state method or correct the name in consumer test
Pact file not published to broker+
Immediate action
Check consumer CI logs for 'Publish pacts' step
Commands
Run consumer test with PactNet.Outputters for debug: set VERBOSE=true
Test broker connection: curl -I https://your-pact-broker/
Fix now
Ensure PactPublisher is configured in consumer's test setup, with correct broker URI and token
Contract Testing vs Integration Testing
AspectContract Testing (PactNet)Integration Testing
Test scopeSingle consumer–provider interactionEnd-to-end flow across multiple services
Environment requiredMock server for consumer; isolated provider instanceAll dependent services must be running
Failure detectionDetects contract violations before deploymentDetects runtime integration issues in test environment
Deployment couplingConsumer and provider can deploy independentlyCoordinated deployment windows required
SpeedFast — seconds per testSlow — minutes per full integration test suite
Maintenance costModerate — provider states require upkeepHigh — managing test data across services is brittle
Who writes testsConsumer team writes; provider team verifiesUsually provider team writes full end-to-end tests
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
ConsumerOrderApiConsumerTests.csusing PactNet;What is Contract Testing in .NET?
ConsumerOrderApiConsumerTests.cspublic class OrderApiConsumerTestsConsumer-Driven Contract Testing with PactNet
ProviderOrderApiProviderTests.csusing PactNet.Verifier;Provider Verification with PactNet
ConsumerOrderApiConsumerTests.cs_pactBuilderHandling Provider States and Edge Cases
ConsumerPactPublisherSetup.csvar publisher = new PactPublisher("https://your-pact-broker.com",CI/CD Integration and Production Gotchas
ConsumerOrderApiConsumerTests.csusing PactNet.Matchers;When Pact Verification Fails
PactAsMigration.cspublic class PactMigrationGateWhy You Should Treat Pact Files Like Database Migrations
PactBrokerSetup.csusing PactNet.Verifier;Pact Brokers Are Not Optional
ProviderVerificationTests.csusing PactNet;Pact Provider Verification with PactNet in .NET
ConsumerTests.csusing PactNet;Consumer-Driven Contract Testing Workflow

Key takeaways

1
Contract testing inverts the testing flow
consumers define expectations, providers prove they meet them.
2
PactNet generates pact files from consumer tests; provider verification replays them against real provider code.
3
Provider states are essential for deterministic test data—but keep them simple and always tear down.
4
Integrate pact verification at the PR gate in both consumer and provider pipelines.
5
Use Pact Broker for versioned pact storage and compatibility tracking.
6
Matchers loosen contracts—only use them for truly dynamic values like timestamps and IDs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does PactNet differ from traditional integration testing for microse...
Q02SENIOR
Explain provider states in PactNet. Why are they necessary?
Q03SENIOR
What are PactNet matchers and when should you use them?
Q04SENIOR
How would you integrate PactNet into a CI/CD pipeline to prevent breakin...
Q01 of 04SENIOR

How does PactNet differ from traditional integration testing for microservices?

ANSWER
PactNet uses consumer-driven contracts: the consumer writes tests that generate a pact file, which the provider verifies independently. No need to deploy all services together. Integration testing requires all dependencies running and is slower. PactNet catches contract violations earlier and allows independent deployments.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is contract testing in .NET using PactNet?
02
Do I need to run the provider service when running consumer tests?
03
Can I use PactNet with non-HTTP protocols?
04
What is the difference between a pact file and an OpenAPI specification?
05
How do I handle authentication headers in PactNet?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Testing. Mark it forged?

7 min read · try the examples if you haven't

Previous
BDD with SpecFlow in C#
5 / 7 · Testing
Next
Playwright for .NET Testing