DEV Community

Cover image for How to Mock gRPC in .NET with WireMock.Net and Protobuf
StepOne
StepOne

Posted on

How to Mock gRPC in .NET with WireMock.Net and Protobuf

TL;DR

WireMock.Net can mock gRPC, but its built-in mechanism requires .proto definitions at runtime and routes body matching through dynamically shaped JSON. That weakens compile-time checks and separates test serialization from the generated protobuf contracts used by the application.

I contributed to the project for several months, learned its internals, and wrote my own extension.

WireMock.Grpc.Protobuf binds requests and responses to generated Google.Protobuf models. It can match an incoming request exactly or evaluate only the fields relevant to a test through a typed predicate:

Request.Create().WithBodyAsGoogleProtobuf(new HelloRequest { Name = "StepOne" });
Request.Create().WithBodyAsGoogleProtobuf((HelloRequest x) => x.Name == "StepOne");
Response.Create().WithBodyAsGoogleProtobuf(new HelloReply { Message = "Hello, StepOne!" });
Enter fullscreen mode Exit fullscreen mode

The rest of the article explains the integration bug that led to the package, the relevant WireMock.Net extension points, and the five-byte gRPC message framing the matcher must handle.

How WireMock.Net Mocks gRPC Requests

First, I should explain how I ended up this deep inside WireMock.Net in the first place.

If you have followed StepOne for a while, you may remember that my journey into the depths of WireMock.Net began with an ordinary bug. At work, I use WireMock to mock gRPC calls to external services and test the transport layer.

// Пример встроенного grpc мока WireMock.Net
// Требуется загрузить .proto файл в память программы
var protoDefinitionText = File.ReadAllText(@"c:\grpc\greet.proto");
var protoDefinitionId = "GrpcGreet";
var server = WireMockServer.Start(useHttp2: true);
server
    .AddProtoDefinition(protoDefinitionId, protoDefinitionText)
    // обратите внимание на отсутствие типизации
    // и возможности контролировать сериализацию JSON
    .Given(Request.Create()
        .UsingPost()
        .WithPath("/grpc/greet.Greeter/SayHello")
        .WithBodyAsProtoBuf("greet.HelloRequest", new JsonMatcher(new
        {
            name = "stef"
        })))
    .WithProtoDefinition(protoDefinitionId)
    .RespondWith(Response.Create()
        .WithHeader("Content-Type", "application/grpc")
        .WithTrailingHeader("grpc-status", "0")
        .WithBodyAsProtoBuf("greet.HelloReply",
            new
            {
                message = "hello {{request.BodyAsJson.name}} {{request.method}}"
            })
        .WithTransformer());
Enter fullscreen mode Exit fullscreen mode

Component tests give us confidence in integrations before code reaches a real environment. Everything should have worked smoothly, but I ran into a nasty problem. As soon as I added several mocks and requests began hitting all of them, the mock server logs filled with errors. Something was clearly wrong.

16.04.2026 13:50:43 [Error] : Getting a Request MatchResult for Mapping 'e4601a45-5ba3-4777-9e8f-f69c2465be93' failed. This mapping will not be evaluated. Exception: System.IO.EndOfStreamException: Attempted to read past the end of the stream.
   at ProtoBuf.ProtoReader.State.ThrowEoF() in /_/src/protobuf-net.Core/ProtoReader.State.ReadMethods.cs:line 809
   at ProtoBuf.ProtoReader.StreamProtoReader.Ensure(State& state, Int32 count, Boolean strict) in /_/src/protobuf-net.Core/ProtoReader.Stream.cs:line 395
   at ProtoBuf.ProtoReader.StreamProtoReader.ImplSkipBytes(State& state, Int64 count) in /_/src/protobuf-net.Core/ProtoReader.Stream.cs:line 403
   at ProtoBuf.ProtoReader.State.ReadWrapped[T](SerializerFeatures features, T value, ISerializer`1 serializer) in /_/src/protobuf-net.Core/ProtoReader.State.ReadMethods.cs:line 1092
   at proto_4(State&, GetCustomerSegmentsRequest)
   at ProtoBuf.Internal.Serializers.SimpleCompiledSerializer`1.ProtoBuf.Serializers.ISerializer<T>.Read(State& state, T value) in /_/src/protobuf-net/Internal/Serializers/CompiledSerializer.cs:line 107
   at ProtoBuf.ProtoReader.State.ReadAsRoot[T](T value, ISerializer`1 serializer) in /_/src/protobuf-net.Core/ProtoReader.State.ReadMethods.cs:line 1157
   at ProtoBuf.ProtoReader.State.DeserializeRoot[T](T value, ISerializer`1 serializer) in /_/src/protobuf-net.Core/ProtoReader.State.ReadMethods.cs:line 1137
   at ProtoBuf.Internal.DynamicStub.ConcreteStub`1.TryDeserializeRoot(TypeModel model, State& state, Object& value, Boolean autoCreate) in /_/src/protobuf-net.Core/Internal/DynamicStub.cs:line 211
   at ProtoBuf.Meta.TypeModel.DeserializeRootAny(State& state, Type type, Object value, Boolean autoCreate) in /_/src/protobuf-net.Core/Meta/TypeModel.cs:line 1083
   at ProtoBuf.ProtoReader.State.DeserializeRootFallback(Object value, Type type) in /_/src/protobuf-net.Core/ProtoReader.State.ReadMethods.cs:line 1247
   at ProtoBuf.Serializer.Deserialize(Type type, Stream source) in /_/src/protobuf-net/Serializer.Deserialize.cs:line 55
   at ProtoBufJsonConverter.Utils.SerializeUtils.ConvertProtoBufToObject(Assembly assembly, String inputTypeFullName, Byte[] protoBufBytes, Boolean skipGrpcHeader)
   at ProtoBufJsonConverter.Converter.ConvertAsync(ConvertToObjectRequest request, CancellationToken cancellationToken)
   at WireMock.Matchers.ProtoBufMatcher.DecodeAsync(Byte[] input, Boolean throwException, CancellationToken cancellationToken)
   at WireMock.Matchers.ProtoBufMatcher.IsMatchAsync(Byte[] input, CancellationToken cancellationToken)
Enter fullscreen mode Exit fullscreen mode

I traced the WireMock source step by step. I checked how protobuf bytes became gRPC C# objects and back again, and precisely where the logic failed.

Then it clicked: the server simply iterated over every stub and tried to convert the bytes into incompatible message types. Imagine a child with a shape sorter. Instead of looking for the square hole, the child methodically tries to push the cube through every opening.

WireMock.Net searching for a mapping for an incoming protobuf message

WireMock.Net searching for a mapping for an incoming protobuf message

I contacted the Netherlands-based author of WireMock, and we began looking for a solution together. During the design discussion, it became clear that a stub needed to reject a request early based on explicit properties such as the URL, headers, cookies, and other metadata. Put simply, the system should immediately pick the square hole for the cube instead of trying every hole in turn. This removed the errors and spared the server unnecessary work.

The discussion is available on GitHub: https://github.com/wiremock/WireMock.Net/issues/1442

Solving the problem across borders was open-source collaboration at its best. My pull request was merged, and the change shipped in a new NuGet package version. The collaboration gave WireMock.Net a new feature: EarlyMismatch.

Request.Create()
    .UsingPost()
    .WithPath("/grpc/greet.Greeter/SayHello")
    .WithEarlyMismatch(RequestMatcherType.Path)
    .WithBodyAsProtoBuf("greet.HelloRequest", new JsonMatcher(new
    {
        name = "stef"
    })));
Enter fullscreen mode Exit fullscreen mode

Matching Generated Protobuf Messages Instead of JSON

Using WireMock in component tests exposed a serious problem.

As soon as the transport layer configured JSON serialization with its own settings, it became clear that WireMock ignored the application's configuration. The mock server processed data according to its own internal rules. As a result, WireMock and the production service interpreted the same contract differently.

The problem lay in the library's architecture. Users could not provide their own JSON settings instance at all. WireMock hid the configuration behind a private modifier and offered no extension points. If you wanted custom settings, you had to rewrite the entire layer.

I proposed changing the API so that settings could be supplied through the constructor. After several review iterations, the PR was accepted: https://github.com/StefH/JsonConverter/pull/22

WireMockServer = WireMockServer.Start(
    new WireMockServerSettings
    {
        DefaultJsonSerializer = new SystemTextJsonConverter(
            new JsonSerializerOptions
            {
                // ...
            }),
    });
Enter fullscreen mode Exit fullscreen mode

At first glance, the change looks tiny—just one new constructor. Yet extension points like this are exactly what let an open-source library fit into a large enterprise project instead of forcing the project to conform to the library.

Capturing WireMock.Net Logs in xUnit

The next improvement addressed another architectural limitation.

WireMock can write logs directly to ITestOutputHelper, which is convenient when debugging the mock server. But as soon as the server moved into an xUnit fixture, that capability practically disappeared. An ITestOutputHelper object does not yet exist when the fixture is created, so the logger required something that xUnit could not yet provide.

You had to choose between a sound test architecture with a shared fixture and convenient WireMock logs.

I suggested a simple idea: accept a Func<ITestOutputHelper> factory instead of a ready-made ITestOutputHelper. The logger would then obtain the current object only when writing a message, after the test had started: https://github.com/wiremock/WireMock.Net/pull/1488

public class WireMockFixture
{
    public WireMockFixture()
    {
        WireMockServer = WireMockServer.Start(
            new WireMockServerSettings
            {
                Logger = new TestOutputHelperWireMockLogger(() => TestOutputHelper)
            });
    }

    public WireMockServer WireMockServer { get; }

    public ITestOutputHelper? TestOutputHelper { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The change required only a few lines of code, but it made WireMock work properly with conventional xUnit fixtures and infrastructure.

Building WireMock.Grpc.Protobuf

Despite my work fixing bugs and improving the architecture, gRPC mocking in WireMock.Net remained weak and awkward:

  • .proto files had to be loaded into memory, which caused failures in GitLab runners.

  • There was no static typing and therefore no IDE completion.

  • Matching worked only through JSON, with no way to configure serialization because the comparison logic used an isolated data-processing pipeline.

That was enough to make me dig deeper and solve the problem by creating my own NuGet package.

WireMock.Grpc.Protobuf is based on a simple idea: a generated Google.Protobuf class is more than a DTO. It already knows the protobuf contract and can turn itself into a binary wire-format message through ToByteArray(). The extension therefore does not need to reload .proto files, build dynamic descriptors, or send the object through JSON. The same IMessage<T> type used by the application remains the source of truth.

Meanwhile, the WireMock.Net server exposes the original request body as a byte[]. The extension only needs to hook into the processing and matching pipeline:

internal sealed class RequestGoogleProtobufMatcher(IObjectMatcher matcher) : IRequestMatcher
{
    public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
    {
        var matchResult = matcher.IsMatch(requestMessage.BodyAsBytes);
        return requestMatchResult.AddMatchDetail(
            new MatchDetail
            {
                Name = matchResult.Name,
                MatcherType = nameof(RequestGoogleProtobufMatcher),
                Score = matchResult.Score,
                Exception = matchResult.Exception
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

The matching logic does not even need to compare C# objects through Equals. It only needs to parse the incoming gRPC message and extract the protobuf payload. The standard is straightforward. Before transmission, a five-byte header is prepended to the payload: one byte stores the compression flag, and the next four store the message length in big-endian format. The raw protobuf bytes follow.

internal sealed class GrpcMessageMatcher<TMessage> : IObjectMatcher
    where TMessage : IMessage<TMessage>
{
    // ...
    private readonly TMessage _messageValue;

    private bool IsMessageMatch(byte[] inputBytes)
    {
        const int compressionFlagIndex = 0;
        const int headerLength = 5;
        if (inputBytes[compressionFlagIndex] != 0)
            return false;
        var sizeHeader = new ReadOnlySpan<byte>(inputBytes, 1, 4);
        var length = BinaryPrimitives.ReadUInt32BigEndian(sizeHeader);
        if (inputBytes.Length - headerLength < length)
            return false;
        var messageBytes = new ReadOnlySpan<byte>(_messageValue.ToByteArray());
        return messageBytes.SequenceEqual(new ReadOnlySpan<byte>(inputBytes, headerLength, (int)length));
    }
}
Enter fullscreen mode Exit fullscreen mode

Matching a mock by request body now comes down to one simple question: do the protobuf bytes received over the network match the bytes of the expected typed object? There is no reflection, JSON, or dynamic deserialization. That is the solution's main strength: the Google.Protobuf generator has already solved the complexity once. It knows field numbers, wire types, and the encoding rules for strings and nested messages. The extension does not try to build yet another model on top of protobuf; it simply uses the contract's existing binary representation.

For less strict scenarios, the package offers another route: a typed Func<T, bool> predicate. We still need to process the incoming message's five-byte header to extract the payload. The resulting bytes are then converted into an object with MessageParser<T> and passed to the supplied predicate.

internal sealed class GrpcPredicateMatcher<TMessage> : IObjectMatcher
    where TMessage : IMessage<TMessage>, new()
{
    // ...
    private static readonly TMessage Empty = new();
    private static readonly MessageParser<TMessage> Parser = new(() => Empty);
    private readonly Func<TMessage, bool> _predicate;

    private bool IsPredicateMatch(byte[] inputBytes)
    {
        const int compressionFlagIndex = 0;
        const int headerLength = 5;
        if (inputBytes[compressionFlagIndex] != 0)
            return false;
        var sizeHeader = new ReadOnlySpan<byte>(inputBytes, 1, 4);
        var length = BinaryPrimitives.ReadUInt32BigEndian(sizeHeader);
        if (inputBytes.Length - headerLength < length)
            return false;
        var inputMessage = Parser.ParseFrom(new ReadOnlySpan<byte>(inputBytes, headerLength, (int)length));
        return _predicate.Invoke(inputMessage);
    }
}
Enter fullscreen mode Exit fullscreen mode

The user chooses the desired strictness: compare the message byte for byte, or check only the fields that matter to the test. In both cases, WireMock remains an off-the-shelf mock server, while responsibility for understanding the protobuf contract stays with the library built for that exact job: Google.Protobuf.

Takeaways

WireMock.Grpc.Protobuf keeps the generated protobuf type as the contract shared by production code and component tests. Exact byte matching is appropriate when the complete message matters; typed predicates keep tests resilient when only selected fields are part of the behavior under test.

The package remains an extension to WireMock.Net rather than a replacement for it. WireMock continues to own the mock server and request pipeline, while Google.Protobuf owns protobuf parsing and serialization.

The implementation and package are available here:

Related .NET Testing Guides

Follow StepOne on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.

Top comments (0)