Intro to PACT for .NET Core: Integration with PactFlow

Introduction
This is in continuation of PACT for .NET series where we will be looking into one of the ways to integrate our APIs with PactFlow.
For a short introduction, PactFlow is a platform that can act as a broker between consumers and providers services/systems. Consumers can push/upload the contracts to PactFlow and provider systems can read the same contracts and try to verify them.
In PactFlow one can visualise failing/passing contracts, total contract integrations, dependency graph and much more. For a better read please visit https://pactflow.io/
You need to register into pact for for a limited free or paid account. For this demo i am using free account that can support up to 2 contracts as per their docs.
Now, for this article we will be following the same Student and Result API scenario, where student is the Provider service and Result service is the consumer service.
To learn more about demo repo that we will be using in this article, please follow previous articles in this series:
Contract testing for APIs: Intro to PACT for .NET Core
Intro to PACT for .NET Core: API contract testing
devcodex.in
Intro to PACT for .NET Core: Events Based Systems
Intro to PACT for .NET Core: Events Based Systems
devcodex.in
Consumer Side
Once the PACT contract file is generated by the consumer, there are several ways we can push or upload the contract to PactFlow. You can read about all the ways at https://docs.pact.io/getting_started/sharing_pacts.
For this post, I have used a combination of their REST APIs and the PactNet library.
The below utility code uploads the contract file to the Pact broker. Among all the parameters, it requires the Pact broker base URI and read/write access token for authentication to the Pact broker. You can obtain this information by logging into your PactFlow account and reaching out to the settings section.

// Code to push pact contract file to pact flow
using System.Net;
using System.Net.Http.Headers;
using System.Text;
namespace PactNet.ConsumerOne.UnitTest.Utilities;
public abstract class PactBrokerUtiliy
{
private const string PactSubUrl = "{0}/pacts/provider/{1}/consumer/{2}/version/{3}";
public static async Task<HttpStatusCode> PublishPactContract(string pactFlowBaseUri,string consumerName, string providerName, string contractJson, string accessToken, string consumerVersion = "")
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
var fullUrl = string.Format(PactSubUrl, pactFlowBaseUri, providerName, consumerName, string.IsNullOrEmpty(consumerVersion) ? Guid.NewGuid() : consumerVersion);
var response =
await httpClient.PutAsync(fullUrl, new StringContent(contractJson, Encoding.UTF8, "application/json"));
var responseContent = await response.Content.ReadAsStringAsync();
return response.StatusCode;
}
}
Code Sample #1 : Code to push pact contract file to pact flow
Now we can use this utility method after the pact contract file is generated by the consumer. Some thing like below
// Code excerpt from ReportCardApiPactFlowTest.cs class
[Fact]
public async Task Get_Student_When_The_StudentId_Is_Invalid()
{
// Arrange
_pactBuilder
.UponReceiving("A GET request to retrieve the student with invalid student id")
.Given("There is student is at least one valid student present")
.WithRequest(HttpMethod.Get, "/students/some-invalid-id")
.WithHeader("Accept", "application/json")
.WillRespond()
.WithStatus(HttpStatusCode.NoContent);
_pactBuilder.Verify(ctx =>
{
// Act
var client = new StudentClient(ctx.MockServerUri);
Assert.ThrowsAsync<Exception>(async () => await client.GetStudentById("some-invalid-id"));
});
await UploadPactContract();
}
private static async Task UploadPactContract()
{
var pactPath = Path.Combine("..",
"..",
"..",
"..",
"pacts",
"ConsumerOne-Student API.json");
var contractJson = File.ReadAllText(pactPath);
var pactFlowBaseUri = Environment.GetEnvironmentVariable("PACT_FLOW_BASE_URL"); // For testing purposes, you may even hardcode this value
var pactFlowToken = Environment.GetEnvironmentVariable("PACT_FLOW_TOKEN"); // For testing purposes, you may even hardcode this value
var statusCode = await PactBrokerUtiliy.PublishPactContract(
pactFlowBaseUri,
"ConsumerOne",
"Student API",
contractJson,
pactFlowToken,
Guid.NewGuid().ToString());
}
Code Sample #2 : Code excerpt from ReportCardApiPactFlowTest.cs class
You may notice that i have used environment variables at few places. This is to avoid mentioning account details that i have used.
Environment.GetEnvironmentVariable("PACT_FLOW_BASE_URL");
// It will translate to something like https://your_account_name.pactflow.io
Environment.GetEnvironmentVariable("PACT_FLOW_TOKEN");
// It will translate to some random string
Code Sample #3 : Code to read environment variables
If you are using it for demo purpose or learning you may also choose to hardcode these to the ones you obtained from pact flow settings page. In case you want to use the code as it is, then is must that you mention these environment variables before you run tests.
After all these code changes and settings are in place, we can run the test and should expect a success. Following things will happen when we run the test.
- Our request verification and assertion will occur.
- Pact contract file will be generated at the specified location.
- The same Pact file content will be read and passed on to the utility method to be sent as payload to HTTP request to PactFlow.
Once the test is green, we can head to pact flow home page to see if any integrations (consumer-provider)/contracts have been uploaded or not. If everything is in order, we should be able to see something like below screenshot.

If you observe the above image, we can see the contract is there, but its in unverified mode. It is because this pact has not been verified yet by the provider. We can see verification in action next.
Provider Side
Provider side verification is relatively straightforward. The following unit test in the provider's unit test class ensures compliance with the contract.
[Fact]
public void Ensure_StudentApi_Honours_Pact_With_ConsumerOne_Using_PactFlow()
{
// Arrange
var config = new PactVerifierConfig
{
Outputters = new List<IOutput>
{
new XunitOutput(_output),
},
LogLevel = PactLogLevel.Information
};
var pactFlowBaseUri = Environment.GetEnvironmentVariable("PACT_FLOW_BASE_URL"); // For testing purposes, you may even hardcode this value
var pactFlowToken = Environment.GetEnvironmentVariable("PACT_FLOW_TOKEN"); // For testing purposes, you may even hardcode this value
// Act // Assert
IPactVerifier pactVerifier = new PactVerifier(config);
pactVerifier
.ServiceProvider("Student API", _fixture.ServerUri)
.WithPactBrokerSource(new Uri(pactFlowBaseUri), configure =>
{
configure.TokenAuthentication(pactFlowToken);
configure.PublishResults(true, "1.0.0"); // Any version
})
.WithProviderStateUrl(new Uri(_fixture.ServerUri, "/provider-states"))
.Verify();
}
Code Sample #4 : Code to verify pact contract
// The below piece of code is the key in verification with pact flow as source
.WithPactBrokerSource(new Uri(pactFlowBaseUri), configure =>
{
configure.TokenAuthentication(pactFlowToken);
configure.PublishResults(true, "1.0.0"); // Any version
})
Code Sample #5 : The below piece of code is the key in verification with pact flow as source
Now finally after all this setup in place. If we run the pact provider test we should see the test being success, if we run it locally. Something like below.

As a result of this we should also be able to see that the pact integration that was showing as unverified earlier has changed to verified.


Failure scenario!
Lets make some changes in our code to deliberately make the code fail. For demo we can change the student service to return firstName1
property in the response instead on firstName
as expected by consumer service. After making this change, when the unit test is run, we can see the below result in pact verification.


For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/pact-net-example
Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com