The four .NET packages

Your configured NuGet source must provide version 0.2.2 before these installation commands can succeed.

Use version 0.2.2 for the examples below. Each example is a complete Program.cs in its own .NET 10 project. The packages also contain .NET 8 and .NET 9 assets. Installing a package alone does not enable mocks or validate contracts.

Before running a network example, configure GAPFY_ECHO_BASE_ADDRESS with your Echo API base address, including a trailing slash, and GAPFY_ECHO_API_KEY through your environment or secret store. Use the API address, not the public mock URL. The corresponding API release must be deployed: the rule feed is GET api/echo/mock/rules. A 404 there is not evidence of an empty rule set.

Gapfy.Echo.Contracts

Publishes a version of a contract your application provides. First create the contract and its Provides binding in Maestro, issue a key with PublishOwnedContracts, and set GAPFY_ECHO_CONTRACT_ID. Save an Echo contract document from the editor as orders.echo.json in the process's working directory. It must be the Echo document, not an example response or an unconverted JSON Schema.

dotnet new console -n EchoContractsDemo -f net10.0
dotnet add EchoContractsDemo package Gapfy.Echo.Contracts --version 0.2.2
using System.Net.Http.Headers;
using Gapfy.Echo.Contracts;

string Required(string name) =>
    Environment.GetEnvironmentVariable(name)
    ?? throw new InvalidOperationException($"Set {name}.");

using var http = new HttpClient
{
    BaseAddress = new Uri(Required("GAPFY_ECHO_BASE_ADDRESS"))
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Required("GAPFY_ECHO_API_KEY"));

var publisher = new EchoContractPublicationClient(http);
var documentJson = await File.ReadAllTextAsync("orders.echo.json");
var publication = await publisher.PublishAsync(
    Guid.Parse(Required("GAPFY_ECHO_CONTRACT_ID")), documentJson);

Console.WriteLine(publication.AssignedVersion.Version);
Console.WriteLine(publication.Created);

The package also exposes the contract codec, validator, inference engine and evaluator. The publication result includes Changes with consumer and producer verdicts. A breaking verdict describes an already completed publication; it does not roll it back. Identical content can return Created = false with an existing version.

Gapfy.Echo.Testing

Checks contracts consumed by the application, using a key with ReadBoundContracts. The call throws on an incompatible change, so you can put the same call in your existing test framework. This example requires the remote service to answer instead of allowing an offline pass.

dotnet new console -n EchoTestingDemo -f net10.0
dotnet add EchoTestingDemo package Gapfy.Echo.Testing --version 0.2.2
using Gapfy.Echo.Testing;

var echo = GapfyEchoValidation.Create(new GapfyEchoValidationOptions
{
    RequireRemote = true
});

await echo.Contracts.ValidateAsync();

On the first successful local run, the package creates EchoContracts snapshots next to the project. Review and commit them. CI refuses to create missing snapshots. Without RequireRemote = true, an unreachable service can produce an offline pass using committed snapshots; its report explains what could not be checked. This is not verification of today's remote contract or the running API. Accept changed snapshots deliberately on a workstation, never automatically in CI.

Gapfy.Echo.Mock.Client

Intercepts outbound calls only for factory-created HttpClient instances you opt in. This example exposes /probe and calls the real URL in PARTNER_API_URL through the opted-in client. Set DOTNET_ENVIRONMENT=Development for local use and provide a ServeMocks key.

dotnet new web -n EchoMockClientDemo -f net10.0
dotnet add EchoMockClientDemo package Gapfy.Echo.Mock.Client --version 0.2.2
using Gapfy.Echo.Mock.Client;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEchoMock(x =>
{
    x.ApiKey = builder.Configuration["GAPFY_ECHO_API_KEY"];
    x.RuleServiceUri = new Uri(
        builder.Configuration["GAPFY_ECHO_BASE_ADDRESS"]
        ?? throw new InvalidOperationException("Set GAPFY_ECHO_BASE_ADDRESS."));
});
builder.Services.AddHttpClient("partner").AddEchoMockInterception();

var app = builder.Build();

app.MapGet("/probe", async (IHttpClientFactory factory) =>
{
    var target = app.Configuration["PARTNER_API_URL"]
        ?? throw new InvalidOperationException("Set PARTNER_API_URL.");
    return await factory.CreateClient("partner").GetStringAsync(target);
});

app.Run();

Static rules can answer locally. Contract-mode rules pass through to the real service in 0.2.2. So do unmatched calls and calls made before the first rules load. A manually constructed HttpClient, a gRPC channel or an SDK with its own transport is not intercepted. The package cannot be armed in Production or Prod; other non-development environments require explicit acknowledgement.

Gapfy.Echo.Mock.Server

Adds mock middleware to your own ASP.NET Core API, using a ServeMocks key. In this example /health remains a real endpoint. Routes that your API has not implemented can answer from matching Echo rules. Place the middleware after routing and before endpoints execute, as shown with WebApplication.

dotnet new web -n EchoMockServerDemo -f net10.0
dotnet add EchoMockServerDemo package Gapfy.Echo.Mock.Server --version 0.2.2
using Gapfy.Echo.Mock.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEchoMock(x =>
{
    x.ApiKey = builder.Configuration["GAPFY_ECHO_API_KEY"];
    x.BaseAddress = new Uri(
        builder.Configuration["GAPFY_ECHO_BASE_ADDRESS"]
        ?? throw new InvalidOperationException("Set GAPFY_ECHO_BASE_ADDRESS."));
});

var app = builder.Build();

app.MapEchoMock();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));

app.Run();

The middleware passes implemented and unmatched routes through. It serves saved response bodies and path substitutions; it does not evaluate the contract-generation document in 0.2.2. It arms outside Production by default. Production requires the explicit EnableInProduction option. With no rules loaded it passes through, and a failed refresh keeps the last cached rules.

Choose the right path