Announcing gRPC JSON transcoding for .NET

James Newton-King

gRPC is a modern way to communicate between apps. gRPC uses HTTP/2, streaming, binary serialization, and message contracts to create high-performance, real-time services. .NET has excellent support for building gRPC apps.

However, like most technology choices, there are trade-offs when choosing gRPC over an alternative like REST+JSON. gRPC’s strengths include performance and developer productivity. Meanwhile, REST+JSON can be used everywhere, and its human-readable JSON messages are easy to debug.

Wouldn’t it be great if we could build services once in ASP.NET Core and get both gRPC and REST? Now you can! Introducing gRPC JSON transcoding for .NET.

gRPC or REST? Why not both

gRPC JSON transcoding is an extension for ASP.NET Core that creates RESTful HTTP APIs for gRPC services. Once configured, JSON transcoding allows you to call gRPC methods with familiar HTTP concepts:

  • HTTP verbs
  • URL parameter binding
  • JSON requests/responses

This is a highly requested feature that was previously available as an experiment. We’re excited to ship the first preview today, and we aim to ship a stable release with .NET 7.

Getting started

  1. The first step is to create a gRPC service (if you don’t already have one). Create a gRPC client and service is a great tutorial for getting started.
  2. Next, add a package reference to Microsoft.AspNetCore.Grpc.JsonTranscoding to the server. Register it in server startup code by adding AddJsonTranscoding(). For example, services.AddGrpc().AddJsonTranscoding().
  3. The last step is annotating your gRPC .proto file with HTTP bindings and routes. The annotations define how gRPC services map to the JSON request and response. You will need to add import "google/api/annotations.proto"; to the gRPC proto file and have a copy of annotations.proto and http.proto in the google/api folder in your project.
syntax = "proto3";

import "google/api/annotations.proto";

package greet;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

In the sample above, the SayHello gRPC method has been annotated with HTTP information. It can now be invoked as gRPC and as a RESTful API:

  • Request: HTTP/1.1 GET /v1/greeter/world
  • Response: { "message": "Hello world" }

And browser apps call it like any other RESTful API:

fetch('https://localhost:5001/v1/greeter/world')
    .then((response) => response.json())
    .then((result) => {
        console.log(result.message);
        // Hello world
    });

This is a simple example. See HttpRule for more customization options.

JSON transcoding vs gRPC-Web

Both JSON transcoding and gRPC-Web allow gRPC services to be called from a browser. However, the way each does this is different:

  • gRPC-Web lets browser apps call gRPC services from the browser with the gRPC-Web client and Protobuf. gRPC-Web requires the browser app to generate a gRPC client and has the advantage of sending small, fast Protobuf messages.
  • JSON transcoding allows browser apps to call gRPC services as RESTful APIs with JSON. The browser app doesn’t need to generate a gRPC client or know anything about gRPC.

About JSON transcoding implementation

JSON transcoding with gRPC isn’t a new concept. grpc-gateway is another technology for creating RESTful JSON APIs from gRPC services. It uses the same .proto annotations to map HTTP concepts to gRPC services. The important difference is in how each is implemented.

grpc-gateway uses code generation to create a reverse-proxy server. The reverse-proxy translates RESTful calls into gRPC+Protobuf and sends them over HTTP/2 to the gRPC service. The benefit of this approach is the gRPC service doesn’t know about the RESTful JSON APIs. Any gRPC server can use grpc-gateway.

Meanwhile, gRPC JSON transcoding runs inside an ASP.NET Core app. It deserializes JSON into Protobuf messages, then invokes the gRPC service directly. We believe JSON transcoding offers many advantages to .NET app developers:

  • Fewer moving parts. Both gRPC services and mapped RESTful JSON API run out of one ASP.NET Core application.
  • Performance. JSON transcoding deserializes JSON to Protobuf messages and invokes the gRPC service directly. There are significant performance benefits in doing this in-process vs. making a new gRPC call to a different server.
  • Cost. Fewer servers = smaller monthly hosting bill.

JSON transcoding doesn’t replace MVC or Minimal APIs. It only supports JSON, and it’s very opinionated about how Protobuf maps to JSON.

What’s next

This is the first release of gRPC JSON transcoding for .NET. Future previews of .NET 7 will focus on improving performance and OpenAPI support.

Try it with .NET today

gRPC JSON transcoding is available now with .NET 7 Preview 4.

For more information about JSON transcoding, check out the documentation, or try out a sample app that uses JSON transcoding.

See it in action

On an recent episode of ASP.NET Community Standup, James Newton-King joined Jon Galloway to demonstrate gRPC Transcoding and answer live questions from viewers:

We look forward to seeing what you create with .NET, gRPC, and now gRPC JSON transcoding!

14 comments

Discussion is closed. Login to edit/delete existing comments.

  • Willy Wonka 0

    Yay! This is exciting.

  • Menashe Tomer 0

    Great.
    Does it support streaming (server and client)?

    • James Newton-KingMicrosoft employee 0

      JSON transcoding supports server streaming methods (i.e. the server streaming messages to the client). The response is written as line delimited JSON objects.

      Client and bidirectional streaming methods aren’t supported.

      • Panagiotis Kanavos 0

        I just tried this with WriteAsync(..,CancellationToken) and got

        System.NotSupportedException: Cancellation of stream writes is not supported by this gRPC implementation.
           at Grpc.Core.IAsyncStreamWriter`1.WriteAsync(T message, CancellationToken cancellationToken)

        To avoid this I had to use WriteAsync(T). The only way I could find to detect whether the call was gRPC or HTTP was to use GetHttpContext() and check the ContentType. There should be an easier way to detect whether the CancellationToken parameter can be used or not.

  • Michael Peter 0

    This is really cool!! Can you say anything regarding openapi support for gRPC Transcoding and documentation comments in .proto files?

    Because if JSON Transcoding can have openapi and documenation it is not a question anymore if REST or gRPC but an option to have both 🙂

    • James Newton-KingMicrosoft employee 0

      OpenAPI currently isn’t supported.

      However, we know it’s really important to have OpenAPI when publishing REST APIs. There will definitely be some support for it in JSON transcoding. We’re looking at the best way to do it now. If we don’t find a solution we’re happy with in .NET 7 we’ll provide an experimental package NuGet package that enables OpenAPI.

      • Richard Espinoza 0

        Hi, one question arises here.
        I understood gRpc more efficient when needed to improve communication between applications, and says that the usage of binary was part of that advantage.

        Now, how this is going to change this approach because I feel we are moving to the web api territory. I’m a bit confused 🤔

  • Viktor Govorov 0

    Is .NET7 target really required. Why are .NET5 and .NET6 not supported?

    • James Newton-KingMicrosoft employee 0

      JSON transcoding requires a lot of custom JSON serialization. System.Text.Json is JSON transcoding’s serializer and it requires some System.Text.Json features new in .NET 7.

  • Majid Shahabfar 0

    Great,
    Do you recommend it for interservice communication for microservices?

  • Srashti Jain 0

    Yeah!!
    I llike it very much. Thank you for sharing this.

  • Dale Sinder 0

    I’ve been experimenting with this. The majority of my gRPC methods require Authorization. I’ve not been able to figure out how to do that for the REST/Json side.
    I’m using JWT and setting the context in each gRPC call that needs authorization. For example:

    Model = await Client.GetHomePageModelAsync(new NoRequest(), myState.AuthHeader);

    Where the myState service provides the JWT in a header from my cached login data:

        public Metadata AuthHeader
        {
            get
            {
                Metadata? headers = new Metadata();
                if (LoginReply is not null && LoginReply.Status == 200)
                    headers.Add("Authorization", $"Bearer {LoginReply.Jwt}");
                return headers;
            }
        }
    
  • Varun Chopra 0

    Hi James
    Thank you for sharing the JSON transcoding feature. I have a particular use case to make the gRPC services compatible with systems that supports HTTP calls in a Kubernetes Cluster. How to access the JSON Transcoded API in the scenario where the dotnet gRPC services are accessed over HTTP?

    I get 400 Bad Request for calls to http endpoint, https endpoint works fine.

    An HTTP/1.x request was sent to an HTTP/2 only endpoint.
  • AKSHAY MISHRA 0

    Rest api call with post is not working on my grpc server api. The body in post api is empty on grpc server (json transcoded through asp.net).
    Grpc-server works fine with grpc-client and the rest-client works fine on a regular asp-net core server (post api is received with body).
    Somehow – when sending the same post message to grpc server – autogenerated code goofs up something and the received message has no data.

    My proto file:

    syntax = "proto3";
    option csharp_namespace = "FileTransfer"; 
    import "google/api/annotations.proto"; // for rest apis gateway
    
    package FastFileTransfer;
    service FileTransfer {
        /* https://localhost:7107/api/v1/health-status */
        rpc HealthStatus(HealthRequest) returns (HealthResponse) { 
            option (google.api.http) = {
                post: "/api/v1/health-status" 
                body: "*"
            };
        }
    }
    
    message HealthRequest {
        string requestId = 1;
        string clientIP = 2;
        string extraData = 3;
    }
    
    message HealthResponse {
        string requestId = 1;
        string serverIP = 2;
        string extraData = 3;
    }

    Server Code:

    public class FFTLogic : FastFileTransfer.FastFileTransferBase
        {
            public override Task HealthStatus(HealthRequest request, ServerCallContext context)
            {
                var ts01 = DateTime.Now.ToString("HH:mm:ss.fffff dd-MMM-yyyy");
                var ed = string.Concat("server-time: ", ts01);
                Console.WriteLine(" ~ client request-id: " + request.RequestId
                    + ", ip: " + request.ClientIP
                    + ", extra-data: " + request.ExtraData);
    
                var response = new HealthResponse 
                {
                    ServerIP = Helper.GetLocalIPAddress(), 
                    RequestId = request.RequestId, 
                    ExtraData = string.Concat(request.ExtraData, ed) 
                };
    
                return Task.FromResult(response);
            }
    }

    Rest client code:

    var client = new HttpClient();
    client.DefaultRequestVersion = new Version(2, 0);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    
    var data = JsonHelper.SerializeToJson(request, false);
    var strContent = new StringContent(data, Encoding.UTF8, "application/json");
    
    var url = "https://localhost:7107/api/v1/health-status"; 
    var httpResponse = await client.PostAsync(url, strContent);
    
    if (httpResponse.IsSuccessStatusCode)
    {
        var resultAsString = await httpResponse.Content.ReadAsStringAsync();
        response = JsonHelper.DeserializeFromJson(resultAsString);
    }
    else { ... }

    Kindly help me on this.

Feedback usabilla icon